# User Invitation System - Complete Implementation Guide
**Version:** 2.0
**Last Updated:** November 18, 2025
**Backend Status:** β
Fully Implemented
**Frontend Status:** π Ready for Implementation
---
## Table of Contents
1. [Overview](#overview)
2. [Complete Workflow](#complete-workflow)
3. [API Endpoints Reference](#api-endpoints-reference)
4. [Frontend Implementation](#frontend-implementation)
5. [Token Expiry & Resend Logic](#token-expiry--resend-logic)
6. [Error Handling](#error-handling)
7. [Security & Validation](#security--validation)
---
## Overview
### System Architecture
```
βββββββββββββββββββ
β Admin Creates β
β Invitation β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ ββββββββββββββββββββ
β Backend Sends βββββββΆβ WhatsApp/Email β
β Notification β β with Invite Link β
βββββββββββββββββββ ββββββββββ¬ββββββββββ
β
βΌ
βββββββββββββββββββ
β User Clicks β
β Link β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β Frontend β
β Validates β
β Token β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β User Fills β
β Registration β
β Form β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β Account β
β Created & β
β Auto Login β
βββββββββββββββββββ
```
### Key Features
β
**Smart Token Management** - Expired tokens auto-regenerate on resend
β
**Dual Notification** - WhatsApp primary, Email fallback
β
**One-Time Use** - Tokens can't be reused after acceptance
β
**Auto Login** - Users logged in immediately after signup
β
**Role-Based** - Pre-assigned role and organization
β
**72-Hour Expiry** - Tokens valid for 3 days (regenerate on resend)
---
## Complete Workflow
### Phase 1: Admin Creates Invitation
**Endpoint:** `POST /api/v1/invitations`
```javascript
const response = await fetch('/api/v1/invitations', {
method: 'POST',
headers: {
'Authorization': `Bearer ${adminToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: "john.doe@example.com",
phone: "+254712345678",
invited_role: "field_agent",
contractor_id: "uuid-of-contractor",
invitation_method: "whatsapp" // or "email" or "both"
})
});
// Response
{
"id": "uuid",
"email": "john.doe@example.com",
"phone": "+254712345678",
"invited_role": "field_agent",
"status": "pending",
"invited_at": "2025-11-18T10:00:00Z",
"expires_at": "2025-11-21T10:00:00Z",
"whatsapp_sent": true,
"email_sent": false,
"organization_name": "ABC Contractors"
}
```
### Phase 2: User Receives Notification
**WhatsApp/Email contains:**
```
π You're invited to join SwiftOps!
ABC Contractors has invited you to join as a Field Agent.
Click here to accept: https://swiftops.atomio.tech/accept-invitation?token=ABC123XYZ
This link expires in 72 hours.
```
### Phase 3: User Clicks Link
**Frontend Route:** `/accept-invitation?token=ABC123XYZ`
```javascript
// Extract token from URL
const searchParams = new URLSearchParams(window.location.search);
const token = searchParams.get('token');
if (!token) {
// Show error: "Invalid invitation link"
}
```
### Phase 4: Validate Token
**Endpoint:** `POST /api/v1/invitations/validate` (PUBLIC - No Auth)
```javascript
const response = await fetch('/api/v1/invitations/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token })
});
if (response.ok) {
const invitation = await response.json();
// Show registration form with pre-filled data
} else {
// Show error message
}
```
**Response:**
```json
{
"id": "uuid",
"email": "john.doe@example.com",
"invited_role": "field_agent",
"status": "pending",
"expires_at": "2025-11-21T10:00:00Z",
"organization_name": "ABC Contractors",
"organization_type": "contractor",
"is_expired": false,
"is_valid": true
}
```
### Phase 5: User Completes Registration
**Endpoint:** `POST /api/v1/invitations/accept` (PUBLIC - No Auth)
```javascript
const response = await fetch('/api/v1/invitations/accept', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: "ABC123XYZ",
first_name: "John",
last_name: "Doe",
password: "SecurePass123!",
phone: "+254712345678" // Optional
})
});
const data = await response.json();
// Store token and redirect
localStorage.setItem('access_token', data.access_token);
window.location.href = '/dashboard';
```
**Response:**
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"user": {
"id": "uuid",
"email": "john.doe@example.com",
"first_name": "John",
"last_name": "Doe",
"full_name": "John Doe",
"role": "field_agent",
"is_active": true
}
}
```
---
## API Endpoints Reference
### 1. Create Invitation
**POST** `/api/v1/invitations`
**Authorization:** Required (`platform_admin`, `client_admin`, `contractor_admin`)
**Request Body:**
```json
{
"email": "user@example.com",
"phone": "+254712345678",
"invited_role": "field_agent",
"client_id": "uuid", // For client roles
"contractor_id": "uuid", // For contractor roles
"invitation_method": "whatsapp" // "whatsapp", "email", "both"
}
```
**Response:** `201 Created`
```json
{
"id": "uuid",
"email": "user@example.com",
"phone": "+254712345678",
"invited_role": "field_agent",
"status": "pending",
"invited_at": "2025-11-18T10:00:00Z",
"expires_at": "2025-11-21T10:00:00Z",
"whatsapp_sent": true,
"email_sent": false,
"organization_name": "ABC Contractors"
}
```
**Authorization Rules:**
- `platform_admin` - Can invite to any organization
- `client_admin` - Can invite to their client only
- `contractor_admin` - Can invite to their contractor only
---
### 2. List Invitations
**GET** `/api/v1/invitations?page=1&per_page=20&status=pending`
**Authorization:** Required (`invite_users` permission)
**Query Parameters:**
- `page` - Page number (default: 1)
- `per_page` - Items per page (default: 20)
- `status` - Filter by status: `pending`, `accepted`, `expired`, `cancelled`
**Response:** `200 OK`
```json
{
"items": [
{
"id": "uuid",
"email": "user@example.com",
"invited_role": "field_agent",
"status": "pending",
"invited_at": "2025-11-18T10:00:00Z",
"expires_at": "2025-11-21T10:00:00Z",
"organization_name": "ABC Contractors"
}
],
"total": 50,
"page": 1,
"per_page": 20,
"pages": 3
}
```
---
### 3. Get Invitation Details
**GET** `/api/v1/invitations/{invitation_id}`
**Authorization:** Required (`invite_users` permission)
**Response:** `200 OK`
```json
{
"id": "uuid",
"email": "user@example.com",
"phone": "+254712345678",
"invited_role": "field_agent",
"status": "pending",
"invited_at": "2025-11-18T10:00:00Z",
"expires_at": "2025-11-21T10:00:00Z",
"accepted_at": null,
"whatsapp_sent": true,
"whatsapp_sent_at": "2025-11-18T10:00:05Z",
"email_sent": false,
"organization_name": "ABC Contractors"
}
```
---
### 4. Resend Invitation
**POST** `/api/v1/invitations/{invitation_id}/resend`
**Authorization:** Required (`invite_users` permission)
**Request Body:**
```json
{
"invitation_method": "email" // Optional: Override delivery method
}
```
**Response:** `200 OK`
```json
{
"id": "uuid",
"email": "user@example.com",
"status": "pending",
"expires_at": "2025-11-21T16:00:00Z", // Extended if was expired
"whatsapp_sent": false,
"email_sent": true
}
```
**Behavior:**
- β
If **not expired**: Resends with same token
- β
If **expired**: Generates NEW token + extends expiry by 72 hours + resends
- β Cannot resend if status is `accepted` or `cancelled`
---
### 5. Cancel Invitation
**DELETE** `/api/v1/invitations/{invitation_id}`
**Authorization:** Required (`invite_users` permission)
**Response:** `204 No Content`
**Behavior:**
- Sets status to `cancelled`
- User can no longer use the token
- Invitation record remains in database for audit trail
---
### 6. Validate Token (PUBLIC)
**POST** `/api/v1/invitations/validate`
**Authorization:** None (Public endpoint)
**Request Body:**
```json
{
"token": "ABC123XYZ"
}
```
**Response:** `200 OK`
```json
{
"id": "uuid",
"email": "john.doe@example.com",
"invited_role": "field_agent",
"status": "pending",
"expires_at": "2025-11-21T10:00:00Z",
"organization_name": "ABC Contractors",
"organization_type": "contractor",
"is_expired": false,
"is_valid": true
}
```
**Use Case:**
- Call this when user lands on `/accept-invitation?token=...`
- Pre-fill email in registration form
- Show organization name and role
- Catch invalid/expired tokens early
---
### 7. Accept Invitation (PUBLIC)
**POST** `/api/v1/invitations/accept`
**Authorization:** None (Public endpoint)
**Request Body:**
```json
{
"token": "ABC123XYZ",
"first_name": "John",
"last_name": "Doe",
"password": "SecurePass123!",
"phone": "+254712345678" // Optional
}
```
**Password Requirements:**
- Minimum 8 characters
- At least 1 uppercase letter
- At least 1 digit
**Phone Requirements:**
- Must start with `+` and country code
- Example: `+254712345678`
**Response:** `200 OK`
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"user": {
"id": "uuid",
"email": "john.doe@example.com",
"first_name": "John",
"last_name": "Doe",
"full_name": "John Doe",
"role": "field_agent",
"is_active": true
}
}
```
**What Happens:**
1. β
Validates token (not expired, still pending)
2. β
Checks if user already exists (prevents duplicates)
3. β
Creates Supabase Auth user
4. β
Creates local user profile with role and organization
5. β
Marks invitation as `accepted`
6. β
Returns authentication token
7. β
User is logged in immediately
---
## Frontend Implementation
### Component Structure
```
src/
βββ pages/
β βββ AcceptInvitationPage.jsx # Main invitation acceptance page
β βββ InvitationExpiredPage.jsx # Show when token expired
βββ components/
β βββ InvitationValidation.jsx # Token validation logic
β βββ RegistrationForm.jsx # User signup form
βββ services/
βββ invitationService.js # API calls
```
---
### AcceptInvitationPage.jsx
```javascript
import React, { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { validateInvitation, acceptInvitation } from '@/services/invitationService';
import RegistrationForm from '@/components/RegistrationForm';
export default function AcceptInvitationPage() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const token = searchParams.get('token');
const [loading, setLoading] = useState(true);
const [invitation, setInvitation] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
if (!token) {
setError('Invalid invitation link');
setLoading(false);
return;
}
validateToken();
}, [token]);
const validateToken = async () => {
try {
const data = await validateInvitation(token);
if (!data.is_valid) {
setError('This invitation is no longer valid');
return;
}
if (data.is_expired) {
setError('This invitation has expired. Please contact your administrator for a new invitation.');
return;
}
setInvitation(data);
} catch (err) {
setError(err.message || 'Failed to validate invitation');
} finally {
setLoading(false);
}
};
const handleSubmit = async (formData) => {
try {
const response = await acceptInvitation({
token,
...formData
});
// Store auth token
localStorage.setItem('access_token', response.access_token);
// Store user info
localStorage.setItem('user', JSON.stringify(response.user));
// Redirect to dashboard
navigate('/dashboard');
// Optional: Show success message
// toast.success('Account created successfully!');
} catch (err) {
throw err; // Let form handle error display
}
};
if (loading) {
return (
);
}
if (error) {
return (
Invitation Error
{error}
);
}
return (
{/* Welcome Header */}
Welcome to SwiftOps!
You've been invited to join {invitation.organization_name}
Role:
{invitation.invited_role.replace('_', ' ')}
{/* Registration Form */}
);
}
```
---
### RegistrationForm.jsx
```javascript
import React, { useState } from 'react';
export default function RegistrationForm({ email, onSubmit }) {
const [formData, setFormData] = useState({
first_name: '',
last_name: '',
password: '',
confirmPassword: '',
phone: ''
});
const [errors, setErrors] = useState({});
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const validatePassword = (password) => {
const errors = [];
if (password.length < 8) errors.push('At least 8 characters');
if (!/[A-Z]/.test(password)) errors.push('One uppercase letter');
if (!/[0-9]/.test(password)) errors.push('One number');
return errors;
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// Clear error for this field
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: null }));
}
};
const handleSubmit = async (e) => {
e.preventDefault();
// Validation
const newErrors = {};
if (!formData.first_name.trim()) {
newErrors.first_name = 'First name is required';
}
if (!formData.last_name.trim()) {
newErrors.last_name = 'Last name is required';
}
const passwordErrors = validatePassword(formData.password);
if (passwordErrors.length > 0) {
newErrors.password = 'Password must contain: ' + passwordErrors.join(', ');
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match';
}
if (formData.phone && !formData.phone.startsWith('+')) {
newErrors.phone = 'Phone must start with + and country code';
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
setLoading(true);
try {
await onSubmit({
first_name: formData.first_name,
last_name: formData.last_name,
password: formData.password,
phone: formData.phone || undefined
});
} catch (err) {
setErrors({
submit: err.response?.data?.detail || 'Failed to create account'
});
} finally {
setLoading(false);
}
};
const passwordStrength = validatePassword(formData.password);
return (
);
}
```
---
### invitationService.js
```javascript
const API_BASE = process.env.REACT_APP_API_URL || 'http://localhost:8000/api/v1';
export const validateInvitation = async (token) => {
const response = await fetch(`${API_BASE}/invitations/validate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to validate invitation');
}
return response.json();
};
export const acceptInvitation = async (data) => {
const response = await fetch(`${API_BASE}/invitations/accept`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to accept invitation');
}
return response.json();
};
export const createInvitation = async (token, data) => {
const response = await fetch(`${API_BASE}/invitations`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to create invitation');
}
return response.json();
};
export const listInvitations = async (token, params = {}) => {
const queryString = new URLSearchParams(params).toString();
const response = await fetch(`${API_BASE}/invitations?${queryString}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to fetch invitations');
}
return response.json();
};
export const resendInvitation = async (token, invitationId, method = null) => {
const response = await fetch(`${API_BASE}/invitations/${invitationId}/resend`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ invitation_method: method })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to resend invitation');
}
return response.json();
};
export const cancelInvitation = async (token, invitationId) => {
const response = await fetch(`${API_BASE}/invitations/${invitationId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to cancel invitation');
}
return true;
};
```
---
### Router Configuration
```javascript
// In your main router file (App.jsx or routes.jsx)
import AcceptInvitationPage from '@/pages/AcceptInvitationPage';
{/* Public Route */}
} />
{/* Other routes */}
} />
} />
{/* ... */}
```
---
## Token Expiry & Resend Logic
### Token Lifecycle
```
βββββββββββββββββββ
β Token Created β
β Expires: +72h β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β Status: Pendingβββββββ User can accept
β Not Expired β
ββββββββββ¬βββββββββ
β
βββββββββββββββ
β β
βΌ βΌ
βββββββββββββββ βββββββββββββββ
β Accepted β β Expired β
β (Used) β β (72h past) β
βββββββββββββββ ββββββββ¬βββββββ
β
βΌ
βββββββββββββββ
β Resend β
β Clicked β
ββββββββ¬βββββββ
β
βΌ
βββββββββββββββββββ
β New Token β
β Generated β
β Expires: +72h β
βββββββββββββββββββ
```
### Resend Behavior
**Scenario 1: Not Expired**
```javascript
// Day 1: Invitation sent, expires Day 4
// Day 2: Admin clicks "Resend"
β Same token sent again
β Expiry unchanged (still Day 4)
β Notification resent via WhatsApp/Email
```
**Scenario 2: Expired**
```javascript
// Day 1: Invitation sent, expires Day 4
// Day 5: Token expired, Admin clicks "Resend"
β NEW token generated
β Expiry set to Day 8 (72 hours from now)
β Notification sent with new link
β Old token no longer works
```
### Admin Dashboard Implementation
```javascript
// InvitationsList.jsx
function InvitationRow({ invitation, onResend }) {
const isExpired = new Date(invitation.expires_at) < new Date();
return (
| {invitation.email} |
{invitation.status}
{isExpired && invitation.status === 'pending' && (
Expired
)}
|
{invitation.invited_role} |
{new Date(invitation.expires_at).toLocaleDateString()} |
{invitation.status === 'pending' && (
)}
|
);
}
```
---
## Error Handling
### Common Error Scenarios
#### 1. Invalid Token
```json
// Response: 400 Bad Request
{
"detail": "Invalid or expired invitation token"
}
```
**Frontend Handling:**
```javascript
if (error.detail.includes('Invalid')) {
setError('This invitation link is invalid. Please check your link or contact support.');
}
```
---
#### 2. Token Expired
```json
// Response: 400 Bad Request
{
"detail": "Invalid or expired invitation token"
}
```
**Frontend Handling:**
```javascript
if (invitation.is_expired) {
setError('This invitation has expired. Please contact your administrator for a new invitation.');
}
```
---
#### 3. User Already Exists
```json
// Response: 400 Bad Request
{
"detail": "User already exists"
}
```
**Frontend Handling:**
```javascript
if (error.detail.includes('already exists')) {
setError('An account with this email already exists. Try logging in instead.');
// Show "Go to Login" button
}
```
---
#### 4. Password Too Weak
```json
// Response: 422 Unprocessable Entity
{
"detail": [
{
"loc": ["body", "password"],
"msg": "Password must contain at least one uppercase letter",
"type": "value_error"
}
]
}
```
**Frontend Handling:**
```javascript
// Show inline validation before submit
const passwordErrors = validatePassword(password);
if (passwordErrors.length > 0) {
setError('Password must contain: ' + passwordErrors.join(', '));
}
```
---
#### 5. Already Accepted
```json
// Response: 404 Not Found
{
"detail": "Invitation not found or already processed"
}
```
**Frontend Handling:**
```javascript
if (error.detail.includes('already processed')) {
setError('This invitation has already been used. Try logging in instead.');
}
```
---
#### 6. Network Error
```javascript
try {
await acceptInvitation(data);
} catch (err) {
if (err.message.includes('Failed to fetch')) {
setError('Connection failed. Please check your internet and try again.');
}
}
```
---
### Error Display Component
```javascript
function ErrorMessage({ error, onRetry, onGoToLogin }) {
if (!error) return null;
const isUserExists = error.includes('already exists');
const isExpired = error.includes('expired');
return (
Error
{error}
{isUserExists && (
)}
{isExpired && (
Contact your administrator for a new invitation.
)}
{!isUserExists && !isExpired && onRetry && (
)}
);
}
```
---
## Security & Validation
### Backend Validation
β
**Token Security**
- 32-byte cryptographically secure random tokens
- Unique constraint in database
- Single-use (marked as accepted after use)
- Time-limited (72-hour expiry)
β
**Password Requirements**
- Minimum 8 characters
- At least 1 uppercase letter
- At least 1 digit
- No maximum length (within reason)
β
**Email Validation**
- Valid email format
- Must match invitation email
- Cannot be changed by user
β
**Phone Validation**
- Optional field
- Must start with `+` and country code
- Example: `+254712345678`
β
**Authorization**
- Only admins with `invite_users` permission
- Row-level security on invitations (org-based)
- Cannot invite to other organizations
### Frontend Validation
```javascript
// Validate before submit
const validate = (formData) => {
const errors = {};
// Required fields
if (!formData.first_name.trim()) {
errors.first_name = 'First name is required';
}
if (!formData.last_name.trim()) {
errors.last_name = 'Last name is required';
}
// Password strength
if (formData.password.length < 8) {
errors.password = 'Password must be at least 8 characters';
}
if (!/[A-Z]/.test(formData.password)) {
errors.password = 'Password must contain an uppercase letter';
}
if (!/[0-9]/.test(formData.password)) {
errors.password = 'Password must contain a number';
}
// Password match
if (formData.password !== formData.confirmPassword) {
errors.confirmPassword = 'Passwords do not match';
}
// Phone format
if (formData.phone && !formData.phone.startsWith('+')) {
errors.phone = 'Phone must start with + and country code';
}
return errors;
};
```
### Best Practices
1. **Validate token on page load** - Don't let user fill form if token is invalid
2. **Show clear error messages** - Help user understand what went wrong
3. **Pre-fill email** - User can't change it (comes from invitation)
4. **Show password requirements live** - Help user create strong password
5. **Auto-login after signup** - Store token and redirect immediately
6. **Handle network errors gracefully** - Show retry option
7. **Disable submit during processing** - Prevent duplicate submissions
8. **Show organization name** - User knows what they're joining
---
## Testing Checklist
### Backend Testing
- [ ] Create invitation with WhatsApp method
- [ ] Create invitation with Email method
- [ ] Create invitation with Both methods
- [ ] Validate valid token
- [ ] Validate expired token
- [ ] Validate invalid token
- [ ] Accept valid invitation
- [ ] Try to accept twice (should fail)
- [ ] Try to accept expired token (should fail)
- [ ] Resend non-expired invitation (same token)
- [ ] Resend expired invitation (new token generated)
- [ ] Cancel invitation
- [ ] Try to accept cancelled invitation (should fail)
### Frontend Testing
- [ ] Land on `/accept-invitation` without token (show error)
- [ ] Land on page with invalid token (show error)
- [ ] Land on page with expired token (show error)
- [ ] Land on page with valid token (show form)
- [ ] Submit form with empty fields (show validation errors)
- [ ] Submit form with weak password (show validation errors)
- [ ] Submit form with mismatched passwords (show validation errors)
- [ ] Submit form with invalid phone format (show validation errors)
- [ ] Submit form with valid data (create account & redirect)
- [ ] Try to accept same invitation twice (show error)
- [ ] Handle network errors gracefully
---
## Environment Variables
```env
# Backend (.env)
APP_DOMAIN=swiftops.atomio.tech
APP_PROTOCOL=https
INVITATION_TOKEN_EXPIRY_HOURS=72
# Notification Services
RESEND_API_KEY=re_xxxxxxxxxxxxx
RESEND_FROM_EMAIL=swiftops@atomio.tech
WASENDER_API_KEY=xxxxxxxxxxxxx
# Supabase
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
```env
# Frontend (.env)
REACT_APP_API_URL=https://api.swiftops.atomio.tech/api/v1
```
---
## Quick Reference
### Key URLs
- Invitation Link: `https://swiftops.atomio.tech/accept-invitation?token=ABC123XYZ`
- API Base: `https://api.swiftops.atomio.tech/api/v1`
### Key Timings
- Token Expiry: 72 hours (3 days)
- Token Regeneration: On resend if expired
- Auto Login: Immediate after account creation
### Key Status Values
- `pending` - Invitation sent, not yet accepted
- `accepted` - User created account
- `expired` - 72 hours passed (can be resent with new token)
- `cancelled` - Admin cancelled invitation
### Key Roles
- `platform_admin` - System administrator
- `client_admin` - Client organization admin
- `contractor_admin` - Contractor organization admin
- `project_manager` - Project manager
- `dispatcher` - Dispatcher
- `sales_manager` - Sales manager
- `field_agent` - Field worker
- `sales_agent` - Sales representative
---
**Last Updated:** November 18, 2025
**Questions?** Check the API responses or backend logs for detailed error messages.