Teams and invitations
Who is in a workspace, what they may do, and which channels they can see. Teams are an access boundary rather than an org chart: their only real power is confining a set of channels.
What is here
Members carry a ROLE, which resolves to permissions. Teams group members and may be confined to channels. Invitations are how somebody becomes a member. All of it hangs off a workspace.
Session credentials only
Every route in this section is declared for sessionToken alone. A machine credential has no team membership and no person to invite, so none of it is reachable with an API key. Most writes take team.manage; the reads take workspace.read.
Members and roles
| Value | Description |
|---|---|
| owner | Everything, including the things only an owner may do. |
| admin | Everything except the owner-only actions. |
| manager | Runs the inbox and the workspace's day-to-day, including analytics. |
| agent | Works conversations. Reads and uses, does not manage. |
| developer | Keys, webhooks, templates and the API surface. |
| billing | The billing screens and nothing else. |
| read_only | Reads. Changes nothing. |
A membership id is a mem_… and is NOT the user id the rest of the API reports for the same human — assigning a conversation takes the membership id, and the session does not carry it, so claiming a thread means looking yourself up in the member list first. Permissions resolved for the caller are on the workspace itself; see authentication.
Invitations
curl -X POST https://whats.azzamkh.sa/api/v1/workspaces/ws_.../invitations \
-H "Authorization: Bearer <access token>" \
-H "Content-Type: application/json" \
-d '{ "email": "agent@example.com", "role": "agent" }'
# 201 Created
# {
# "id": "inv_...",
# "email": "agent@example.com",
# "role": "agent",
# "status": "pending",
# "expires_at": "2026-08-21T09:00:00.000Z",
# "created_at": "2026-08-14T09:00:00.000Z"
# }
# The invitee accepts with the token from the email. This route names no
# workspace and takes no scope — the token carries both.
curl -X POST https://whats.azzamkh.sa/api/v1/invitations/accept \
-H "Authorization: Bearer <access token>" \
-H "Content-Type: application/json" \
-d '{ "token": "..." }'
# 200 OK
# { "workspace_id": "ws_...", "role": "agent", "already_member": false }POST /v1/invitations/acceptnames no workspace and takes NO scope — the token carries both. It answersalready_memberrather than failing when the person is already in, so accepting twice is safe.- An invitation carries an
expires_atand astatus. Revoking is a delete on the invitation, and it answers the id and the new status. - Inviting is limited to 50 an hour per workspace, and accepting to 20 an hour per person.
Teams
POST …/teamsis idempotent on the workspace and name: it returns the EXISTING team rather than refusing, so there is no name-taken error to report.- Deleting a team, removing a member and removing a team member all answer
removed— notdeleted. GET …/teamsis cursor paginated.…/teams/:id/membersand…/teams/:id/channelsare NOT — they answer a plaindataarray.- A team member row carries both
member_idanduser_id. Themember_idcan be an empty string when the workspace membership row is gone, so do not key a list on it blindly.
Channel access
Granting a channel RESTRICTS it
A channel nobody has granted is visible to everyone holding messages.read. The first grant confines it to the teams that hold it. So PUT …/channels with an empty array does not hide anything — it removes THIS team's grants, and the channel becomes workspace-wide again if no other team holds it. Conversation access is filtered by the same rule, on reads and writes alike.
# Granting INVERTS the intuition: a channel nobody has confined is visible
# to everyone with messages.read, and the first grant confines it.
curl -X PUT https://whats.azzamkh.sa/api/v1/workspaces/ws_.../teams/tm_.../channels \
-H "Authorization: Bearer <access token>" \
-H "Content-Type: application/json" \
-d '{ "channel_ids": ["ch_..."] }'
# 200 OK
# { "team_id": "tm_...", "channel_ids": ["ch_..."] }
# Sending [] does not hide anything: it removes THIS team's grants, and
# the channel becomes workspace-wide again if no other team holds it.Errors that matter here
| Code | HTTP | When it happens |
|---|---|---|
| TEAM_NOT_FOUND | 404 | No team with that id in this workspace. |
| MEMBER_NOT_FOUND | 404 | No such membership. |
| INVITATION_NOT_FOUND | 404 | No such invitation, or it is no longer pending. |
| MEMBER_ALREADY_EXISTS | 409 | That person is already a member of this workspace. |
| OWNER_REQUIRED | 403 | Only the workspace owner may do that. |
Every code and its status is on the errors page.