Skip to content

Accounts & Organizations

Sema Link supports multi-organization access. A single user can belong to multiple organizations, each with its own contacts, campaigns, credits, and sender IDs. Organization membership is tracked in the account_members junction table.

Base path: /api/v1/accounts

All endpoints require Authorization: Bearer <access_token>.


Multi-Org Model

The active JWT encodes the currently selected accountId and role — both sourced from the account_members row for that user-org pair. To switch context, call POST /api/v1/accounts/switch and use the returned token pair for subsequent requests.


Endpoints

GET /api/v1/accounts

Returns all organizations the authenticated user belongs to, with a flag indicating which is currently active (matches the JWT's accountId).

Response — 200 OK

json
[
  {
    "userId": "<uuid>",
    "accountId": "<uuid>",
    "accountName": "Acme Corp",
    "accountStatus": "active",
    "role": "owner",
    "isCurrent": true
  },
  {
    "userId": "<uuid>",
    "accountId": "<uuid>",
    "accountName": "Side Project Ltd",
    "accountStatus": "active",
    "role": "member",
    "isCurrent": false
  }
]

GET /api/v1/accounts/me

Returns the full details of the currently active organization.

Response — 200 OK

json
{
  "id": "<uuid>",
  "name": "Acme Corp",
  "type": "direct",
  "status": "active",
  "country": "KE",
  "timezone": "Africa/Nairobi",
  "address": "123 Kimathi Street",
  "city": "Nairobi",
  "state": null,
  "zipCode": null,
  "createdAt": "2025-01-01T00:00:00.000Z"
}

PATCH /api/v1/accounts/me

Updates the current organization's profile. Requires owner or admin role — member returns 403 Forbidden.

Request Body (all fields optional)

json
{
  "name": "Acme Corp Kenya",
  "timezone": "Africa/Nairobi",
  "country": "KE",
  "address": "123 Kimathi Street",
  "city": "Nairobi",
  "state": null,
  "zipCode": null
}
FieldTypeNotes
namestring1–100 characters
timezonestringIANA timezone identifier (e.g. Africa/Nairobi)
countrystringISO 3166-1 alpha-2, exactly 2 characters
addressstring | nullStreet address, max 255 characters
citystring | nullMax 100 characters
statestring | nullProvince/state, max 100 characters
zipCodestring | nullPostal code, max 20 characters

Response — 200 OK — returns the updated account object (same shape as GET /me).

Errors

CodeCause
403 ForbiddenRole is member
422 UnprocessableValidation failed (e.g. country is not 2 chars)

POST /api/v1/accounts

Creates a new organization and immediately switches into it. The calling user becomes the owner.

On creation the API:

  1. Inserts the new accounts row
  2. Sets billing_account_id to the new account's own ID
  3. Creates a credits row (zero balance, currency KES)
  4. Inserts an account_members row with role: 'owner'
  5. Revokes all active refresh tokens for the previous org
  6. Issues a new access + refresh token pair bound to the new org

Request Body

json
{
  "name": "Side Project Ltd"
}
FieldRequiredNotes
nameYesMust be unique among your own organizations

Response — 201 Created

json
{
  "accessToken": "<jwt>",
  "refreshToken": "<opaque-token>",
  "user": {
    "id": "<uuid>",
    "firstName": "Jane",
    "lastName": "Doe",
    "email": "jane@example.com",
    "role": "owner"
  },
  "account": {
    "id": "<uuid>",
    "name": "Side Project Ltd"
  }
}

Store both tokens exactly as you would after a normal login — the new pair is scoped to the new org.

Errors

CodeCause
409 ConflictYou already have an org with that name
422 UnprocessableName is empty

POST /api/v1/accounts/switch

Switches the active organization context. The API verifies the user has a valid account_members row for the target org, then revokes all active refresh tokens for the current org and issues a new token pair bound to the target.

Request Body

json
{
  "accountId": "<target-org-uuid>"
}

Response — 200 OK

json
{
  "accessToken": "<jwt>",
  "refreshToken": "<opaque-token>",
  "user": {
    "id": "<uuid>",
    "firstName": "Jane",
    "lastName": "Doe",
    "email": "jane@example.com",
    "role": "member"
  }
}

Replace your stored tokens with the new pair. All subsequent requests must use the new accessToken — the previous one remains valid only until its 15-minute TTL expires but will no longer match the active org context.

Errors

CodeCause
403 ForbiddenNo account_members row for this user + target org
422 UnprocessableaccountId missing from request body

Internal use only — Sema Link Engineering