Workspaces
A workspace is the tenant: every channel, contact, conversation, key and invoice belongs to exactly one, and nothing crosses between them. It also owns the clock the rest of the platform is measured by.
The workspace
A person may belong to several, so a user access token alone is not enough to identify one — X-Workspace-Id is. An API key already names its workspace and ignores the header.
| Field | Type | Description |
|---|---|---|
| id | string | The workspace's public id, and the value of X-Workspace-Id. |
| name, slug | string | The display name and its slug. |
| status | string | Active or suspended. |
| is_personal | boolean | Whether it is the personal workspace created with the account. |
| locale | string | The default language for platform-generated copy. |
| timezone | string | The one clock. See below. |
| role | string | The CALLER's role in it. Present on the routes that resolve a membership. |
| permissions | string[] | The caller's resolved permissions. A UI signal only — the backend re-derives them on every request. |
The timezone is not a display preference
It is the clock business hours are evaluated in, the clock the away reply decides against, and the clock every analytics bucket is cut with. Changing it moves all three. It is never the caller's browser timezone, and deriving one locally is the bug those features exist to prevent.
Endpoints
curl -X POST https://whats.azzamkh.sa/api/v1/workspaces \
-H "Authorization: Bearer <access token>" \
-H "Content-Type: application/json" \
-d '{ "name": "Acme", "locale": "ar", "timezone": "Asia/Riyadh" }'
# 201 Created — role and permissions are resolved for THIS user.
# {
# "id": "ws_...",
# "name": "Acme",
# "slug": "acme",
# "status": "active",
# "is_personal": false,
# "locale": "ar",
# "timezone": "Asia/Riyadh",
# "created_at": "2026-08-14T09:00:00.000Z",
# "role": "owner",
# "permissions": ["workspace.read", "workspace.manage", ...]
# }- Every route in this section is declared for
sessionTokenalone — an API key belongs to a workspace and cannot create, list or archive one. See authentication. DELETEARCHIVES. It answers the id and the new status, and it does not destroy the tenant's data.- Creating a workspace is limited to 20 an hour per person.
GET /v1/auth/mereturns the USER only. Memberships come fromGET /v1/workspaces, which is also whereroleand the resolvedpermissionslive.
Naming the workspace on a request
Tenant context is explicit everywhere. There is no ambient default, deliberately: a key minted into the wrong workspace is not recoverable. See authentication.
| Credential | Header | Result |
|---|---|---|
| API key | — | The key names its workspace. The header is ignored. |
| Access token | X-Workspace-Id | The header decides, and it is required. |
| Access token | — | 400 WORKSPACE_CONTEXT_REQUIRED. |
People and money
Members, roles, invitations and teams are all workspace sub-resources and have their own page: teams and invitations. The subscription and its statements are on billing.
Errors that matter here
| Code | HTTP | When it happens |
|---|---|---|
| WORKSPACE_NOT_FOUND | 404 | No such workspace, or the caller is not a member of it. The two are one answer on purpose. |
| WORKSPACE_CONTEXT_REQUIRED | 400 | A user access token was used without X-Workspace-Id on a route that needs one. |
| WORKSPACE_SUSPENDED | 403 | The workspace is suspended. Reads may still work; work that would send does not. |
| OWNER_REQUIRED | 403 | Only the workspace owner may do that. |
Every code and its status is on the errors page.