Invitations
Invitations are used to invite new users to join a client account (company) in Snapbooks. The invitation system handles the complete workflow from sending invitation emails to user account creation and role assignment.
Key Features
- Multi-language Support: Automatic email templates in English and Norwegian
- User-friendly Role Names: Displays “Accountant” instead of cryptic codes like “AA”
- Secure Token System: Each invitation has a unique, secure token with 7-day expiration
- Complete Workflow: Email sending, user registration, and account setup in one flow
- Permission Control: Role-based access with proper validation
Endpoints
Method | Endpoint | Description | Authentication |
---|---|---|---|
POST | /api/v2/invitations | Create a new invitation | Required |
GET | /api/v2/invitations | List invitations | Required |
GET | /api/v2/invitations/{id} | Get invitation details | Required |
DELETE | /api/v2/invitations/{id} | Cancel an invitation | Required |
GET | /public/v2/invitations/{token} | Get invitation by token | Public |
POST | /public/v2/invitations/{token}/accept | Accept invitation | Public |
Authenticated Endpoints
Create Invitation
POST /api/v2/invitations
Creates a new invitation and optionally sends an email to the invited user.
Request Body
{
"email": "john@example.com",
"client_account_id": 123,
"role_id": 2
}
Parameters
Parameter | Type | Required | Description |
---|---|---|---|
string | Yes | Email address of the user to invite | |
client_account_id | integer | Yes | ID of the client account to invite user to |
role_id | integer | Yes | ID of the role to assign (2=AA, 3=CA, 4=BK, 5=EM) |
Headers
Header | Required | Description | Example |
---|---|---|---|
Accept-Language | No | Language preference for email template | "no" (Norwegian) or "en" (English) |
Role Codes & Display Names
Role ID | Code | English | Norwegian | Account Type | Notes |
---|---|---|---|---|---|
2 | AA | Accountant | Autorisert regnskapsfører | Provider only | Full accounting access for accounting firms |
3 | CA | Client Account Owner | Kontoeier | All accounts | Account owner with admin rights |
4 | BK | Bookkeeper | Regnskapsfører | Provider only | Bookkeeping tasks for accounting firms |
5 | EM | Employee | Ansatt | All accounts | Basic employee access |
Provider Accounts: Companies with industrial classification 69.2xx (accounting/auditing firms)
Regular Accounts: All other company types
Response (201 Created)
{
"id": 456,
"created_at": "2023-10-15T14:30:00Z",
"updated_at": "2023-10-15T14:30:00Z",
"email": "john@example.com",
"client_account_id": 123,
"role_id": 2,
"token": "abc123def456...",
"status": "PENDING",
"expires_at": "2023-10-22T14:30:00Z",
"invited_by_id": 789,
"accepted_by_id": null,
"accepted_at": null,
"is_expired": false,
"is_pending": true,
"can_be_accepted": true,
"client_account": {
"id": 123,
"display_name": "ACME Corp AS",
"unique_name": "acme-corp"
},
"role": {
"id": 2,
"name": "AA"
},
"invited_by": {
"id": 789,
"first_name": "Admin",
"last_name": "User",
"email": "admin@acme.com"
}
}
List Invitations
GET /api/v2/invitations
Lists invitations with optional filtering and pagination.
Query Parameters
Parameter | Type | Required | Description |
---|---|---|---|
client_account_id | integer | No | Filter by client account ID |
status | string | No | Filter by status (PENDING, ACCEPTED, EXPIRED, CANCELLED) |
limit | integer | No | Number of items per page (default: 100, max: 1000) |
offset | integer | No | Number of items to skip (default: 0) |
Example Request
GET /api/v2/invitations?client_account_id=123&status=PENDING&limit=50
Response (200 OK)
{
"invitations": [
{
"id": 456,
"invited_email": "john@example.com",
"status": "PENDING",
"expires_at": "2023-10-22T14:30:00Z",
"is_expired": false,
"can_be_accepted": true,
"client_account": {
"id": 123,
"display_name": "ACME Corp AS"
},
"role": {
"id": 2,
"name": "AA"
}
}
],
"count": 1,
"limit": 50,
"offset": 0
}
Get Invitation
GET /api/v2/invitations/{id}
Gets detailed information about a specific invitation.
Response (200 OK)
{
"id": 456,
"created_at": "2023-10-15T14:30:00Z",
"invited_email": "john@example.com",
"status": "PENDING",
"expires_at": "2023-10-22T14:30:00Z",
"is_expired": false,
"is_pending": true,
"can_be_accepted": true,
"client_account": {
"id": 123,
"display_name": "ACME Corp AS",
"unique_name": "acme-corp"
},
"role": {
"id": 2,
"name": "AA"
},
"invited_by": {
"id": 789,
"first_name": "Admin",
"last_name": "User",
"email": "admin@acme.com"
},
"accepted_by": null
}
Cancel Invitation
DELETE /api/v2/invitations/{id}
Cancels a pending invitation. Cannot cancel already accepted invitations.
Response (200 OK)
Returns the updated invitation with status set to “CANCELLED”.
Public Endpoints
These endpoints do not require authentication and are used in the invitation acceptance flow.
Get Invitation by Token
GET /public/v2/invitations/{token}
Gets invitation details using the secure token from the email link.
Response (200 OK)
{
"id": 456,
"invited_email": "john@example.com",
"status": "PENDING",
"expires_at": "2023-10-22T14:30:00Z",
"is_expired": false,
"can_be_accepted": true,
"client_account": {
"id": 123,
"display_name": "ACME Corp AS"
},
"role": {
"id": 2,
"name": "AA"
}
}
Accept Invitation
POST /public/v2/invitations/{token}/accept
Accepts an invitation and creates a new user account. This is the final step in the invitation workflow.
Request Body
{
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"password": "SecurePassword123!"
}
Parameters
Parameter | Type | Required | Description |
---|---|---|---|
first_name | string | Yes | User’s first name |
last_name | string | Yes | User’s last name |
string | Yes | User’s email (can be different from invited email) | |
password | string | Yes | User’s password (must meet security requirements) |
Response (200 OK)
{
"message": "Invitation accepted successfully",
"invitation": {
"id": 456,
"status": "ACCEPTED",
"accepted_at": "2023-10-16T10:15:00Z",
"client_account": {
"id": 123,
"display_name": "ACME Corp AS"
},
"role": {
"id": 2,
"name": "AA"
}
},
"user": {
"id": 999,
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"email_verified": true
},
"tokens": {
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"token_type": "Bearer"
}
}
Data Model
Invitation Attributes
Attribute | Type | Description |
---|---|---|
id | integer | The unique ID of the invitation |
created_at | datetime | When the invitation was created |
updated_at | datetime | When the invitation was last updated |
invited_email | string | The email address that was invited |
client_account_id | integer | The ID of the client account |
role_id | integer | The ID of the role to assign |
token | string | The secure token for acceptance (64 characters) |
status | string | Current status (PENDING, ACCEPTED, EXPIRED, CANCELLED) |
expires_at | datetime | When the invitation expires (7 days from creation) |
invited_by_id | integer | The ID of the user who sent the invitation |
accepted_by_id | integer | The ID of the user who accepted (null if not accepted) |
accepted_at | datetime | When the invitation was accepted (null if not accepted) |
Computed Properties
Property | Type | Description |
---|---|---|
is_expired | boolean | Whether the invitation has passed its expiry date |
is_pending | boolean | Whether the invitation status is PENDING |
can_be_accepted | boolean | Whether the invitation can currently be accepted |
Relationships
Relationship | Type | Description |
---|---|---|
client_account | ClientAccount | The client account the user is invited to |
role | Role | The role that will be assigned |
invited_by | User | The user who sent the invitation |
accepted_by | User | The user who accepted the invitation |
Frontend Integration Guide
Complete Invitation Flow
- Admin Sends Invitation
const response = await fetch('/api/v2/invitations', { method: 'POST', headers: { 'Authorization': 'Bearer ' + accessToken, 'Content-Type': 'application/json', 'Accept-Language': 'no' // Norwegian email template }, body: JSON.stringify({ invited_email: 'newuser@company.com', client_account_id: 123, role_id: 2 }) });
- User Receives Email with beautiful template containing:
- Company name and role (displayed as “Regnskapsfører” not “AA”)
- Invitation link with secure token
- Expiration date (7 days)
- User Clicks Link - Frontend loads invitation details:
const invitation = await fetch(`/public/v2/invitations/${token}`); if (invitation.can_be_accepted) { // Show registration form }
- User Accepts - Creates account and logs in:
const result = await fetch(`/public/v2/invitations/${token}/accept`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ first_name: 'John', last_name: 'Doe', email: 'john@example.com', password: 'SecurePass123!' }) }); // User is now logged in with tokens localStorage.setItem('access_token', result.tokens.access_token);
Team Management UI Examples
Invitation List Component:
// Load invitations for a client account
const invitations = await fetch('/api/v2/invitations?client_account_id=123&status=PENDING');
// Display with user-friendly role names
invitations.data.forEach(invitation => {
const roleDisplay = getRoleDisplayName(invitation.role.name, 'EN');
console.log(`${invitation.invited_email} - ${roleDisplay}`);
});
function getRoleDisplayName(code, lang = 'EN') {
const roles = {
EN: { AA: 'Accountant', CA: 'Client Owner', BK: 'Bookkeeper', EM: 'Employee' },
NO: { AA: 'Autorisert regnskapsfører', CA: 'Kontoeier', BK: 'Regnskapsfører', EM: 'Ansatt' }
};
return roles[lang][code] || code;
}
Cancel Invitation:
const cancelInvitation = async (invitationId) => {
const response = await fetch(`/api/v2/invitations/${invitationId}`, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + accessToken }
});
if (response.ok) {
// Remove from UI or refresh list
refreshInvitationList();
}
};
Error Handling
Common error scenarios and HTTP status codes:
Error | Status | Description |
---|---|---|
Missing required field | 400 | Request validation failed |
User already exists | 400 | Email already has an account |
AA and BK roles are only available for provider client accounts | 400 | Role not allowed for account type |
Invalid invitation token | 404 | Token not found or malformed |
Invitation expired | 400 | Past expiration date |
Already accepted | 400 | Cannot accept twice |
Permission denied | 401/403 | User lacks access |
Example Error Response:
{
"error": "Invitation has expired",
"status": 400
}
Multi-language Support
The system automatically selects email templates based on the language
parameter:
- English (
"EN"
): Clean, professional invitation emails - Norwegian (
"NO", "NB", "NN"
): Localized Norwegian content with proper role translations
Templates include company branding with the distinctive peach color (rgb(255, 209, 190)) and responsive design.
Best Practices
- Check Permissions: Verify user can invite to the client account
- Validate Emails: Use proper email validation before sending
- Handle Duplicates: System automatically cancels existing pending invitations
- Show Progress: Indicate email sending status to users
- Auto-refresh: Update invitation lists after actions
- Expiry Warnings: Show expiry dates prominently
- Role Clarity: Always display user-friendly role names
Notes
- Invitations expire after 7 days and cannot be extended
- Users can change their email during acceptance (different from invited email)
- The system sends both HTML and plain text email versions
- Email sending failures don’t block invitation creation (graceful degradation)
- All dates are returned in ISO 8601 format with UTC timezone
- Role assignments become active immediately upon acceptance