Appearance
Users
Generated from
DialerDigital/corecommit0fc18008b3c8— specopenapi/dialer-v1.yamlv0.134.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.
Dashboard user accounts (email + password; disabled, never deleted).
Endpoints
| Method | Path | Summary |
|---|---|---|
GET | /v1/users | List dashboard users and open invitations |
POST | /v1/users | Invite a person to the dashboard |
DELETE | /v1/users/{id} | Disable a user (never deletes) |
POST | /v1/users/{id}/enable | Re-enable a disabled user |
POST | /v1/users/{id}/mfa/reset-failures | Lift a person's second-factor brake |
POST | /v1/users/{id}/mfa/remove | Remove a person's second factor (lost authenticator and recovery codes) |
List dashboard users and open invitations
http
GET /v1/usersEmails, names, status and login timestamps — password hashes are never exposed. invitations lists the OPEN invitations (neither accepted nor superseded) sent by POST /v1/users, newest first: pending until they expire, expired after. An invitation to an address that is already a user of another tenant is listed, and expires, exactly like one nobody opened — this list says nothing about who else the address belongs to. An accepted invitation leaves this list and its person appears in users.
Responses
200
The tenant's dashboard users and its open invitations.
| Field | Type | Required | Description |
|---|---|---|---|
users | array of User | yes | |
users[].id | string (uuid) | no | |
users[].email | string (email) | no | Global login identifier, stored lowercase. |
users[].name | string | no | |
users[].status | active · disabled | no | |
users[].role | member · account_admin | no | Intra-account privilege. Only an account_admin may create or modify the empresa-wide rows every sibling sede inherits (scope: "empresa" on carriers and SMS providers); a member gets 403. Read-only here: promotion is an operator gesture, never a request. |
users[].password_updated_at | string (date-time) | no | |
users[].last_login_at | string (date-time) | null | no | |
users[].mfa_enabled | boolean | no | Whether the person has an ACTIVE second factor (an enrolment still pending confirmation is false). Emitted ONLY when the caller is an account_admin dashboard login — the one who can act on it with POST /v1/users/{id}/mfa/remove; for a member, a dd_ key, or when the caller's role cannot be determined the field is ABSENT (not false). Wherever a User is rendered, the same rule holds: GET /v1/users, GET /v1/me, the user returned by DELETE /v1/users/{id} and POST /v1/users/{id}/enable, and the user in the session responses (POST /v1/auth/login, POST /v1/auth/sso/exchange, and the two MFA completion routes) — there, for the account_admin who just signed in. |
users[].created_at | string (date-time) | no | |
users[].updated_at | string (date-time) | no | |
invitations | array of UserInvitation | yes | |
invitations[].id | string (uuid) | no | |
invitations[].email | string (email) | no | Stored lowercase. |
invitations[].name | string | no | |
invitations[].status | pending · expired | no | pending until expires_at, expired after. Resend by inviting again. |
invitations[].expires_at | string (date-time) | no | |
invitations[].created_at | string (date-time) | no | |
invitations[].invited_by | object | no | Who made the invitation (core#2449), immutable. kind is user (a dashboard login; user_id is that person), api_key (a dd_ key; user_id null) or operator. kind is null for an invitation that predates the record. |
invitations[].invited_by.kind | user · api_key · operator · null | yes | |
invitations[].invited_by.user_id | string,null (uuid) | yes | |
truncated | boolean | yes | true when there are more rows than the 500 users or 500 open invitations (it is true if either list was cut): the response is a prefix in the documented order, not the whole set. |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Invite a person to the dashboard
http
POST /v1/usersInvites — it does not create the user (ADR #182, a breaking change of this operation: it used to take a password and answer 201 with the created user). It records an invitation and e-mails the address a single-use link (valid 72 h) to /accept-invite; the person is created when they redeem it with the password THEY choose, through POST /v1/auth/password-reset/confirm. An address is therefore only taken by someone who proved they read its mail.
The answer is 202 with the SAME body — byte for byte — whether the address is free or already a user of ANOTHER tenant. In the second case the invitation is recorded and listed all the same, and the address receives a notice that it already has an account (with no link): the inviting tenant learns nothing about other tenants. Only an address that is already a user of THIS tenant, active or disabled, is a 409 — that row is in this tenant's own GET /v1/users anyway.
Role rules (core#2334, core#2449): once the tenant has an active account_admin, this takes an account_admin login (403 role_forbidden for a member AND for a dd_ key; per-tenant knob user_management_admin_only, default on; founder knob api_key_manages_people, default off, gives the dd_ key its old reach back). A tenant with no active account_admin keeps the old posture, dd_ key included (it invites the first person). The invitation records who made it (invited_by).
Inviting the same address again supersedes the previous link (that is how an invitation is resent). Two brakes bound the mail this route can send: per tenant (a visible 429) and per address across every tenant (invisible: over it the invitation is still recorded and the answer is the same 202, but no mail leaves — a 429 there would tell one tenant that another just invited that address).
Bootstrap runs with the dd_ machine key (it invites the FIRST user); after that any ddu_ session can manage users (all users are console admins day-1). tenant_id is never read from the body. A password in the body is a 400: the invited person chooses it.
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
email | string (email) | yes | |
name | string | no | |
locale | es · en | no | Language the inviting admin has the console in; the invitation (and the "this address already has an account" notice) goes out in it. Absent or anything else: the cell's language (DEFAULT_LOCALE). Never an error. |
json
{
"email": "[email protected]",
"name": "Ops Admin",
"locale": "es"
}Responses
202
Invitation recorded. Identical for every recorded outcome (see the description).
| Field | Type | Required | Description |
|---|---|---|---|
status | invited | yes | |
message | string | yes |
json
{
"status": "invited",
"message": "invitation recorded"
}400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — UserManagementForbidden
A user-management write was refused (core#2334, ADR #46 addendum). code admin_target_forbidden: a member login tried to disable or re-enable an account_admin — always, whatever the tenant's settings. code role_forbidden: the tenant has an active account_admin and the caller is a member login or (core#2449) a dd_ key (the route takes an account_admin login; founder knob api_key_manages_people, default off, restores the key); the per-tenant knob user_management_admin_only (default on) turns this one off, and a tenant with no active account_admin is not asked for one. code forbidden: the tenant is suspended or the token is a widget token.
json
{
"error": {
"code": "role_forbidden",
"message": "managing users takes a dashboard login with the account_admin role"
}
}409
The address is already a user of THIS tenant (active or disabled), or a concurrent invitation to it from this same tenant won the race.
json
{
"error": {
"code": "conflict",
"message": "that address is already a user of this tenant"
}
}429
This tenant's invitation brake refused the request (fixed window; retryable later). code rate_limited. Keyed per tenant only — it says nothing about the address. Retry-After is the time left in the fixed window.
json
{
"error": {
"code": "rate_limited",
"message": "user invitation rate limit exceeded; retry later"
}
}Disable a user (never deletes)
http
DELETE /v1/users/{id}Sets status: disabled AND revokes every live session the user holds (their bearer dies at the next request). Idempotent. Users are audit evidence — rows never delete; re-enable via POST /v1/users/{id}/enable.
Refused with 409 when it would lock the account out (core#2328): cannot_disable_self when the caller is the target (a dashboard login; a dd_ key is no person), and last_account_admin when the target is the only active account_admin of the tenant. An already-disabled user stays idempotent.
Role rules (core#2334): once the tenant has an active account_admin, this takes a dd_ key or an account_admin login (403 role_forbidden for a member AND, since core#2449, for a dd_ key; per-tenant knob user_management_admin_only, default on; founder knob api_key_manages_people, default off). A tenant with no active account_admin keeps the old posture, dd_ key included. A member can never disable an account_admin (403 admin_target_forbidden).
Keys the person minted (core#2359): the response lists every dd_ key their session minted in issued_api_keys (live ones have revoked_at: null), so the administrator can rotate what a leaver left behind. Per-tenant knob user_disable_revokes_api_keys (default off): when on, disabling the person first revokes their live keys.
Seat and SIP credential (core#2405): per-tenant knob user_disable_retires_seat (default on). The agent linked to the person leaves the agent FSM (armed while a call is in flight), its agentcred entry and usrloc binding are evicted and its widget login is switched off; seat_retirement reports each step (null with the knob off). POST /v1/auth/agent-login refuses an agent whose linked person is not active.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The disabled user and the API keys that person minted.
| Field | Type | Required | Description |
|---|---|---|---|
user | object (User) | yes | A dashboard user — the password hash is never exposed. |
user.id | string (uuid) | no | |
user.email | string (email) | no | Global login identifier, stored lowercase. |
user.name | string | no | |
user.status | active · disabled | no | |
user.role | member · account_admin | no | Intra-account privilege. Only an account_admin may create or modify the empresa-wide rows every sibling sede inherits (scope: "empresa" on carriers and SMS providers); a member gets 403. Read-only here: promotion is an operator gesture, never a request. |
user.password_updated_at | string (date-time) | no | |
user.last_login_at | string (date-time) | null | no | |
user.mfa_enabled | boolean | no | Whether the person has an ACTIVE second factor (an enrolment still pending confirmation is false). Emitted ONLY when the caller is an account_admin dashboard login — the one who can act on it with POST /v1/users/{id}/mfa/remove; for a member, a dd_ key, or when the caller's role cannot be determined the field is ABSENT (not false). Wherever a User is rendered, the same rule holds: GET /v1/users, GET /v1/me, the user returned by DELETE /v1/users/{id} and POST /v1/users/{id}/enable, and the user in the session responses (POST /v1/auth/login, POST /v1/auth/sso/exchange, and the two MFA completion routes) — there, for the account_admin who just signed in. |
user.created_at | string (date-time) | no | |
user.updated_at | string (date-time) | no | |
issued_api_keys | array of ApiKey | yes | |
issued_api_keys[].id | string (uuid) | no | |
issued_api_keys[].label | string | no | |
issued_api_keys[].revoked_at | string (date-time) | null | no | |
issued_api_keys[].last_used_at | string (date-time) | null | no | Rotation-hygiene clock: the last time this key authenticated SUCCESSFULLY. Written at most once a minute per key, so a fresh reading may lag by up to that much. null means it has not authenticated successfully since the column acquired a writer — not that the key was never presented. |
issued_api_keys[].allowed_cidrs | array of string | no | Network allowlist (core#2450). [] = unrestricted. Outside it a request is served and counted, or refused with 401, per the tenant knob api_key_network_enforced (default off). |
issued_api_keys[].stale | boolean | no | true for a live key unused for 90 days (a key that never authenticated counts from its creation). Advice for the console to rotate or revoke; nothing expires on its own. |
issued_api_keys[].created_by_user_id | string (uuid) | null | no | The dashboard user whose session minted the key (core#2359). null when another dd_ key, the operator or the seeds did, and on every key that predates the field. |
issued_api_keys[].created_at | string (date-time) | no | |
seat_retirement | object,null | no | What happened to the linked agent's seat; null when user_disable_retires_seat is off. |
seat_retirement.seat | retired · armed · none · failed | no | |
seat_retirement.sip_teardown | ok · skipped · failed | no | |
seat_retirement.widget_login | disabled · none · failed | no |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — UserManagementForbidden
A user-management write was refused (core#2334, ADR #46 addendum). code admin_target_forbidden: a member login tried to disable or re-enable an account_admin — always, whatever the tenant's settings. code role_forbidden: the tenant has an active account_admin and the caller is a member login or (core#2449) a dd_ key (the route takes an account_admin login; founder knob api_key_manages_people, default off, restores the key); the per-tenant knob user_management_admin_only (default on) turns this one off, and a tenant with no active account_admin is not asked for one. code forbidden: the tenant is suspended or the token is a widget token.
json
{
"error": {
"code": "role_forbidden",
"message": "managing users takes a dashboard login with the account_admin role"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409
Refused: cannot_disable_self (the caller is the target) or last_account_admin (the target is the only active account_admin).
json
{
"error": {
"code": "last_account_admin",
"message": "the last active account_admin cannot be disabled; promote another one first"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Re-enable a disabled user
http
POST /v1/users/{id}/enableSessions revoked at disable STAY revoked — the user logs in fresh.
Role rules (core#2334): once the tenant has an active account_admin, this takes a dd_ key or an account_admin login (403 role_forbidden for a member AND, since core#2449, for a dd_ key; per-tenant knob user_management_admin_only, default on; founder knob api_key_manages_people, default off). A tenant with no active account_admin keeps the old posture, dd_ key included. A member can never re-enable an account_admin (403 admin_target_forbidden).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The re-enabled user.
| Field | Type | Required | Description |
|---|---|---|---|
user | object (User) | yes | A dashboard user — the password hash is never exposed. |
user.id | string (uuid) | no | |
user.email | string (email) | no | Global login identifier, stored lowercase. |
user.name | string | no | |
user.status | active · disabled | no | |
user.role | member · account_admin | no | Intra-account privilege. Only an account_admin may create or modify the empresa-wide rows every sibling sede inherits (scope: "empresa" on carriers and SMS providers); a member gets 403. Read-only here: promotion is an operator gesture, never a request. |
user.password_updated_at | string (date-time) | no | |
user.last_login_at | string (date-time) | null | no | |
user.mfa_enabled | boolean | no | Whether the person has an ACTIVE second factor (an enrolment still pending confirmation is false). Emitted ONLY when the caller is an account_admin dashboard login — the one who can act on it with POST /v1/users/{id}/mfa/remove; for a member, a dd_ key, or when the caller's role cannot be determined the field is ABSENT (not false). Wherever a User is rendered, the same rule holds: GET /v1/users, GET /v1/me, the user returned by DELETE /v1/users/{id} and POST /v1/users/{id}/enable, and the user in the session responses (POST /v1/auth/login, POST /v1/auth/sso/exchange, and the two MFA completion routes) — there, for the account_admin who just signed in. |
user.created_at | string (date-time) | no | |
user.updated_at | string (date-time) | no |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — UserManagementForbidden
A user-management write was refused (core#2334, ADR #46 addendum). code admin_target_forbidden: a member login tried to disable or re-enable an account_admin — always, whatever the tenant's settings. code role_forbidden: the tenant has an active account_admin and the caller is a member login or (core#2449) a dd_ key (the route takes an account_admin login; founder knob api_key_manages_people, default off, restores the key); the per-tenant knob user_management_admin_only (default on) turns this one off, and a tenant with no active account_admin is not asked for one. code forbidden: the tenant is suspended or the token is a widget token.
json
{
"error": {
"code": "role_forbidden",
"message": "managing users takes a dashboard login with the account_admin role"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Lift a person's second-factor brake
http
POST /v1/users/{id}/mfa/reset-failuresClears the cumulative brake of the second factor (core#1572) for one person of the tenant: writes a failures_reset event authored by the calling admin, and the refused codes before it stop counting. Nothing is deleted from the audit trail. account_admin dashboard login only (see 403).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
204
The failure count was reset.
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Remove a person's second factor (lost authenticator and recovery codes)
http
POST /v1/users/{id}/mfa/removeThe recovery path for a person who lost BOTH the authenticator and the recovery codes (core#1580): an account_admin of the SAME tenant removes that person's active second factor. In ONE transaction — if any step fails the factor is left intact: the factor and its unused recovery codes stop being valid, EVERY live ddu_ session of the person is revoked, and a factor_removed_by_admin event (author = the calling admin, target = the person, the written reason) is appended to the second-factor audit trail. Nothing is deleted.
It opens no door without a second factor: when the tenant requires it (mfa_required_enforced) the person's next login lands in the forced enrollment; otherwise the account simply has no second factor until they enrol again.
account_admin dashboard login only (see 403): never the person themself (they have no factor to prove) and never a dd_ key. An admin cannot use it on THEMSELVES (409): their own path is POST /v1/me/mfa/disable, which re-authenticates. A disabled person is 404 — re-enable them first.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | yes | Why the factor is being removed — free text, kept in the audit trail. Leading and trailing whitespace is trimmed before counting. |
json
{
"reason": "Lost phone and printed codes; identity confirmed on a video call."
}Responses
204
The factor was removed, the person's sessions revoked and the event written.
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409
Nothing to remove or not this route: the person has no ACTIVE second factor, or the caller is that same person (use POST /v1/me/mfa/disable).
429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}