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
email 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
email 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

  1. 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
      })
    });
    
  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)
  3. User Clicks Link - Frontend loads invitation details:
    const invitation = await fetch(`/public/v2/invitations/${token}`);
    if (invitation.can_be_accepted) {
      // Show registration form
    }
    
  4. 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

  1. Check Permissions: Verify user can invite to the client account
  2. Validate Emails: Use proper email validation before sending
  3. Handle Duplicates: System automatically cancels existing pending invitations
  4. Show Progress: Indicate email sending status to users
  5. Auto-refresh: Update invitation lists after actions
  6. Expiry Warnings: Show expiry dates prominently
  7. 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