API reference
People, invites and workspaces
/v1/docs-json, browsable at /v1/docs under Workspaces and Team — people); this page is the map.Retrying writes
The writes that create or change something take Idempotency-Key: every /v1/team/* write, POST /v1/workspaces, DELETE /v1/workspaces/{id}, POST /v1/me/workspaces/{id}/leave and the workspace settings. Four routes take none, because a retry is already safe:
POST /v1/auth/switch-workspace: switching twice to the same workspace lands in the same place.POST /v1/auth/invites/acceptandPOST /v1/me/invites/{id}/accept: an invite is used once, so a retried accept answersalready_a_member(409) and changes nothing.POST /v1/auth/signup: a second signup with the same username or e-mail never creates a second account; when the answer was lost, sign in instead.
Idempotency keys are kept per workspace, so POST /v1/workspaces from a session without a workspace (after signing up from an invite, or after leaving the last one) has none: before retrying a create whose answer you did not get, read GET /v1/me/workspaces. A replayed create answers idempotency_secret_not_replayable (409, its answer carries tokens) with the workspace in details.resource_id.
People and invites (owners)
| Route | Who | What |
|---|---|---|
GET /v1/team/members | team.read | People of the workspace, never synthetic accounts; cursor paginated ({data, pagination}). |
PATCH /v1/team/members/{id} | team.write | Change a role (role). Demoting an owner takes synthetic_accounts dispositions. |
GET /v1/team/members/{id}/removal-preview | team.write | What removing or demoting the person affects: synthetic accounts mailed to them, what they created, whom they invited, their key count. |
DELETE /v1/team/members/{id} | team.write | Remove, with synthetic_accounts (move_to_me | rotate | archive; accounts whose password they only know default to rotate) and created_items (disable | keep). |
POST /v1/team/invites | team.write | Invite email with role. 201 with the invite, never its link. |
GET /v1/team/invites | team.read | Pending, expired and bounced invites (30 days), newest first; cursor paginated (pagination), plus inviting_paused_until. |
POST /v1/team/invites/{id}/resend | team.write | Mail a new link; the old one stops working. |
DELETE /v1/team/invites/{id} | team.write | Cancel a pending invite. 204. |
A person is id wm_…, an invite id wi_…. Inviting or promoting someone to Owner, and demoting or removing an owner, need a second factor from the last 5 minutes; inviting or promoting to Full access, the last 15 (step_up_required).
Accepting
| Route | Who | What |
|---|---|---|
POST /v1/auth/invites/inspect | Public | What a link is for: workspace, role, inviter_email, email_masked, expires_at. Uses nothing up. |
POST /v1/auth/invites/accept | Signed in | Join with the link's token. Answers workspace, role and a session switched into it. |
GET /v1/me/invites | Signed in | Pending invites to any verified address of your account, newest first; cursor paginated (pagination). |
POST /v1/me/invites/{id}/accept | Signed in | Accept one of them by id, under the same rules as the link. |
POST /v1/auth/signup | Public | Takes an optional invite_token: the account gets no sandbox, and accepts after verifying its address. |
Workspaces
| Route | Who | What |
|---|---|---|
GET /v1/me/workspaces | Signed in | Your workspaces with environment_label, role, is_current, member_count, owner_emails, and your limits; cursor paginated (pagination), one page at the default limit. |
POST /v1/workspaces | Signed-in person | Create one: name, environment_label, environment_color, company, copy_settings_from. 201, switched into it. |
POST /v1/auth/switch-workspace | Signed-in person | Tokens for workspace_id. |
POST /v1/me/workspaces/{id}/leave | Member | Leave; synthetic_accounts takes archive for accounts mailed to you; accounts whose password you know are reset unless you send archive. |
GET /v1/workspaces/{id}/deletion-preview | Owner | Counts of what goes, and step_up_required. |
DELETE /v1/workspaces/{id} | Owner | Delete with confirm_name. 202 with purge_after. |
GET /v1/auth/me (a session) and GET /v1/whoami (a session or a key) name the workspace a credential acts in, with its label (workspace, workspace), and say whether it is a person (is_human, is_human). auth/me also carries features (multi_workspace); its workspace is null for a session without a workspace.
X-DM-Workspace
An optional request header on every authenticated route: X-DM-Workspace: tnt_…, the workspace the request is meant for. When the session or key acts in another one the API answers workspace_mismatch (409) and does nothing, on reads as well as writes. An empty header counts as absent; any other value that is not the current workspace is a mismatch. An API key always acts in its own workspace, so for a key the header is a check, never a way to reach another workspace.
Error codes
| Status | code | Meaning |
|---|---|---|
| 400 | synthetic_address | You invited the sign-in address of one of this workspace’s synthetic accounts. |
| 400 | disposition_required | Removing, demoting or leaving needs a choice for each synthetic account mailed to the person (listed in details). |
| 400 | disposition_not_allowed | move_to_me was sent for a synthetic account whose mail does not go to the person (listed in details): use rotate or archive. |
| 400 | confirm_name_mismatch | confirm_name is not the workspace’s name (case counts; surrounding spaces do not). |
| 401 | account_revoked | You are no longer a member of the workspace this token acts in. A person keeps their other workspaces: refresh, then switch. |
| 401 | session_revoked | The token was issued before your current membership began (you were removed and invited again). Refresh or sign in again. |
| 401 | api_key_not_allowed | These routes take a signed-in session, never an API key. |
| 403 | step_up_required | Prove a second factor first (details.max_age_minutes: 5 for owner changes and deletion, 15 for Full access), then refresh the token and retry. |
| 403 | synthetic_account_not_allowed | Synthetic accounts cannot create workspaces or accept invites. |
| 403 | workspace_switch_not_allowed | Synthetic accounts cannot switch workspaces. |
| 403 | invite_email_mismatch | The invited address is not a verified address of your account. Add and verify it (/v1/me/emails), then accept. |
| 403 | email_not_verified | Your own e-mail must be verified to invite or create workspaces (POST /v1/workspaces and a bootstrap that makes one). |
| 404 | workspace_not_found | No such workspace, or you are not a member of it (the same answer for both). |
| 404 | invite_invalid | Not an invite link, or not a pending invite to one of your verified addresses. |
| 409 | last_owner | The change would leave the workspace without an owner. |
| 409 | already_a_member | The address or account is already a person in this workspace. |
| 409 | invite_pending | That address already has a pending invite (details.invite_id): resend it. |
| 409 | invite_not_pending | The invite was accepted or cancelled. |
| 409 | invite_undeliverable | We can’t deliver mail to this address; nothing was sent. |
| 409 | invite_paused | A recipient marked one of your invites as spam: inviting is paused for 24 hours (details.retry_after_s). |
| 409 | invite_pending_limit | 25 pending invites in this workspace. |
| 409 | member_limit_reached | 50 people in this workspace. |
| 409 | membership_limit_reached | You belong to 50 workspaces. |
| 409 | workspace_limit_reached | You created and still own 10 workspaces (POST /v1/workspaces and POST /v1/auth/bootstrap alike). |
| 409 | workspace_create_in_progress | Another create of yours is running; retry after details.retry_after_s. |
| 409 | cannot_remove_self | Use leave for yourself. |
| 409 | member_changed | The person’s role changed while you were deciding; read it again. |
| 409 | synthetic_mail_owner | Synthetic accounts are still mailed to you: archive them, or ask another owner to move their mail first. |
| 409 | mail_owner_departed | This synthetic account’s mail goes to someone who is no longer an owner: move it first. |
| 409 | workspace_mismatch | X-DM-Workspace names another workspace than the one this credential acts in (details.current_workspace_id). Nothing was done. |
| 409 | no_active_workspace | The session has no workspace: switch or create one. Only account and workspace routes work without one. |
| 410 | invite_expired | The invite expired. Ask an owner for a new one. |
| 410 | invite_used | The invite was already accepted. |
| 410 | invite_revoked | The invite was cancelled or replaced by a resend. |
| 429 | invite_rate_limited | An invite limit was reached; retry after Retry-After. |
| 429 | workspace_create_rate_limited | 5 workspaces created for you in 24 hours by any route (deleted ones count); retry after details.retry_after_s. |
| 502 | auth_unavailable | Our sign-in service failed mid-way; nothing changed. Retry. |
| 503 | invite_unavailable | Invite mail is paused platform-wide; try later. |
In the audit trail
Invites, acceptances, role changes, removals, leaving, creating, deleting and switching are audit actions (workspace_invite.*, workspace_member.*, tenant.create, tenant.delete, auth.switch_workspace); see the audit catalogue.