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
[
{
"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
{
"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)
{
"name": "Acme Corp Kenya",
"timezone": "Africa/Nairobi",
"country": "KE",
"address": "123 Kimathi Street",
"city": "Nairobi",
"state": null,
"zipCode": null
}| Field | Type | Notes |
|---|---|---|
name | string | 1–100 characters |
timezone | string | IANA timezone identifier (e.g. Africa/Nairobi) |
country | string | ISO 3166-1 alpha-2, exactly 2 characters |
address | string | null | Street address, max 255 characters |
city | string | null | Max 100 characters |
state | string | null | Province/state, max 100 characters |
zipCode | string | null | Postal code, max 20 characters |
Response — 200 OK — returns the updated account object (same shape as GET /me).
Errors
| Code | Cause |
|---|---|
403 Forbidden | Role is member |
422 Unprocessable | Validation 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:
- Inserts the new
accountsrow - Sets
billing_account_idto the new account's own ID - Creates a
creditsrow (zero balance, currencyKES) - Inserts an
account_membersrow withrole: 'owner' - Revokes all active refresh tokens for the previous org
- Issues a new access + refresh token pair bound to the new org
Request Body
{
"name": "Side Project Ltd"
}| Field | Required | Notes |
|---|---|---|
name | Yes | Must be unique among your own organizations |
Response — 201 Created
{
"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
| Code | Cause |
|---|---|
409 Conflict | You already have an org with that name |
422 Unprocessable | Name 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
{
"accountId": "<target-org-uuid>"
}Response — 200 OK
{
"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
| Code | Cause |
|---|---|
403 Forbidden | No account_members row for this user + target org |
422 Unprocessable | accountId missing from request body |