Skip to content

API keys ​

Generated from DialerDigital/core commit bbb8e373d900 — spec openapi/dialer-v1.yaml v0.151.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.

Español

Per-tenant API keys (sha256 at rest; plaintext shown once).

Endpoints ​

MethodPathSummary
GET/v1/api_keysList API keys
POST/v1/api_keysMint an API key
PATCH/v1/api_keys/{id}Set the network allowlist of an API key
DELETE/v1/api_keys/{id}Revoke an API key
POST/v1/api_keys/{id}/rotateRotate an API key in one step, with an overlap
GET/v1/api_keys/{id}/requestsRequests made with one API key
GET/v1/api_keys/{id}/requests/summaryRequests per minute of one API key, with its 429s

List API keys ​

http
GET /v1/api_keys

Ids, labels and revocation timestamps only — hashes are never exposed.

Responses

200 ​

The tenant's keys.

FieldTypeRequiredDescription
api_keysarray of ApiKeyyes
api_keys[].idstring (uuid)no
api_keys[].labelstringno
api_keys[].environmentlive · testnolive (every key that predates the field) or test: a test key reaches no outbound path. Fixed at creation.
api_keys[].revoked_atstring (date-time) | nullno
api_keys[].last_used_atstring (date-time) | nullnoRotation-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.
api_keys[].allowed_cidrsarray of stringnoNetwork allowlist. [] = unrestricted. Outside it a request is served and counted, or refused with 401, per the tenant knob api_key_network_enforced (default off).
api_keys[].stalebooleannotrue 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; a key expires on its own only when it has an expires_at.
api_keys[].permissionsarray of ApiKeyPermission | nullnoRoute groups the key may enter. null = all (every key that predates the field).
api_keys[].expires_atstring (date-time) | nullnoInstant from which the key stops authenticating; null = never. A rotation sets it on the old key to the end of the overlap.
api_keys[].created_by_user_idstring (uuid) | nullnoThe dashboard user whose session minted the key. null when another dd_ key, the operator or the seeds did, and on every key that predates the field.
api_keys[].created_atstring (date-time)no
json
{
  "api_keys": [
    {
      "id": "7a3f0f7e-52a1-4f3d-9f9d-27e5a1b9c001",
      "label": "onboarding",
      "revoked_at": null,
      "created_at": "2026-06-01T12:00:00Z"
    }
  ]
}

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"
  }
}

429 — ApiKeyRateLimited ​

This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (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"
  }
}

Mint an API key ​

http
POST /v1/api_keys

The plaintext token (dd_...) is returned ONCE and only its sha256 is stored. Rotate in one step with POST /v1/api_keys/{id}/rotate.

environment is live (default; the historic dd_ prefix) or test (token prefix dd_test_). A test key authenticates and reads through the same lanes and RLS as a live one, but reaches no outbound path: the routes that dial, send SMS, buy numbers or provision carriers answer 403 test_environment, and nothing is metered or charged. The same answer covers the routes that mint, set or reset a live credential or identity: POST|DELETE /agents/{id}/credential, POST /api_keys/{id}/rotate, POST /users and its enable, mfa/reset-failures and mfa/remove, POST|PATCH /click-to-call/sites, POST /widget_tokens (and /sip_credential) and PUT|PATCH /sso/idp-config. The environment is fixed at creation.

Role rules: minting takes an account_admin login (403 role_forbidden for a member AND, once the tenant has an active account_admin, for a dd_ key; per-tenant knob api_key_admin_only, default on; founder knob api_key_manages_people, default off). A tenant with no active account_admin keeps the dd_ key minting. A key minted from a dashboard login records that person in created_by_user_id; every mint is written to the key audit trail.

allowed_cidrs is the key's network allowlist; omitted or [] is no restriction. What a request from outside the list does is the tenant knob api_key_network_enforced: off (the default) serves and counts it, on answers 401.

permissions restricts the key to route GROUPS; absent or null is the key of always (every route). A key with a list gets 403 permission_denied on the routes of any other group (e.g. /v1/billing/* without billing); the group of a route is its first path segment, and a segment no group claims belongs to general. A key that itself holds a list cannot mint one with more (403 permission_escalation). expires_at (ISO 8601, in the future; absent = never) is the instant from which the key stops authenticating (401, like a revoked one); other keys are unaffected.

Limits: a tenant holds at most api_keys_max_live LIVE keys (default 50; 409 api_key_limit over it, revoking a key frees a slot), and minting is rate-limited per tenant (429 rate_limited with Retry-After). The request quota is per key, so a tenant's keys together are also bounded by api_tenant_requests_per_minute (default 5x one key's).

Request body (JSON)

FieldTypeRequiredDescription
labelstringnoFree-form label (default "").
environmentlive · testnotest mints a key that never reaches calls, SMS or email (dd_test_ prefix). Immutable. Any other value is 422.
allowed_cidrsarray of stringnoCIDRs (or bare addresses) the key may be used from. A list with an entry that does not parse is rejected whole (400); a value that is not a list of strings is 422.
permissionsarray of ApiKeyPermission | nullnoRoute groups the key may enter. Absent or null = all. [] = none. An unknown group is 422.
expires_atstring (date-time) | nullnoInstant from which the key stops authenticating. Must be in the future (else 422).
json
{
  "label": "postman-smoke-1751600000"
}

Responses

201 ​

Key minted; token is the only time the plaintext is visible.

FieldTypeRequiredDescription
api_keyobject (ApiKey)yesKey metadata — the token hash is never exposed.
api_key.idstring (uuid)no
api_key.labelstringno
api_key.environmentlive · testnolive (every key that predates the field) or test: a test key reaches no outbound path. Fixed at creation.
api_key.revoked_atstring (date-time) | nullno
api_key.last_used_atstring (date-time) | nullnoRotation-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.
api_key.allowed_cidrsarray of stringnoNetwork allowlist. [] = unrestricted. Outside it a request is served and counted, or refused with 401, per the tenant knob api_key_network_enforced (default off).
api_key.stalebooleannotrue 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; a key expires on its own only when it has an expires_at.
api_key.permissionsarray of ApiKeyPermission | nullnoRoute groups the key may enter. null = all (every key that predates the field).
api_key.expires_atstring (date-time) | nullnoInstant from which the key stops authenticating; null = never. A rotation sets it on the old key to the end of the overlap.
api_key.created_by_user_idstring (uuid) | nullnoThe dashboard user whose session minted the key. null when another dd_ key, the operator or the seeds did, and on every key that predates the field.
api_key.created_atstring (date-time)no
tokenstringyesPlaintext bearer token, shown exactly once.
json
{
  "api_key": {
    "id": "9a1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
    "label": "postman-smoke-1751600000",
    "revoked_at": null,
    "created_at": "2026-07-05T10:00:00Z"
  },
  "token": "dd_example_plaintext_shown_once"
}

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 — ApiKeyAdminForbidden ​

Minting or revoking an API key was refused. code role_forbidden: the caller is a dashboard login without the account_admin role, or a dd_ key in a tenant that already has an active account_admin (founder knob api_key_manages_people, default off); the per-tenant knob api_key_admin_only (default on) turns this off.

json
{
  "error": {
    "code": "role_forbidden",
    "message": "minting or revoking an API key takes a dashboard login with the account_admin role; a dd_ key may do it only while the tenant has no account_admin"
  }
}

409 — ApiKeyLimit ​

The tenant already holds the maximum number of LIVE API keys (per-tenant knob api_keys_max_live, default 50). code api_key_limit. Nothing was minted; revoke a key (DELETE /v1/api_keys/{id}) and retry.

json
{
  "error": {
    "code": "api_key_limit",
    "message": "this tenant already holds the maximum number of live API keys; revoke one first"
  }
}

422 — Unprocessable ​

Shape is fine, semantics are not (broken domain rule).

json
{
  "error": {
    "code": "unprocessable",
    "message": "scheduled_at is closer than the minimum notice for a callback (10 minutes unless the tenant's callback_min_notice_seconds says otherwise)"
  }
}

429 — ApiKeyRateLimited ​

This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (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"
  }
}

Set the network allowlist of an API key ​

http
PATCH /v1/api_keys/{id}

Replaces allowed_cidrs of a LIVE key; [] lifts the restriction. Takes the same roles as minting (403 role_forbidden for a member while the knob api_key_admin_only is on). The change is written to the key audit trail as network_changed and binds the next request: the key is forgotten by the authentication cache. A revoked or unknown key is 404. A key that holds a permission list may change only itself and keys whose permissions it covers: otherwise 403 permission_escalation.

A test API key may act only on test keys: against a live key it is refused with 403 test_environment and the key is left untouched.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON, required)

FieldTypeRequiredDescription
allowed_cidrsarray of stringyes
json
{
  "allowed_cidrs": [
    "203.0.113.0/24",
    "2001:db8::/32"
  ]
}

Responses

200 ​

The key with its new list.

FieldTypeRequiredDescription
api_keyobject (ApiKey)yesKey metadata — the token hash is never exposed.
api_key.idstring (uuid)no
api_key.labelstringno
api_key.environmentlive · testnolive (every key that predates the field) or test: a test key reaches no outbound path. Fixed at creation.
api_key.revoked_atstring (date-time) | nullno
api_key.last_used_atstring (date-time) | nullnoRotation-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.
api_key.allowed_cidrsarray of stringnoNetwork allowlist. [] = unrestricted. Outside it a request is served and counted, or refused with 401, per the tenant knob api_key_network_enforced (default off).
api_key.stalebooleannotrue 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; a key expires on its own only when it has an expires_at.
api_key.permissionsarray of ApiKeyPermission | nullnoRoute groups the key may enter. null = all (every key that predates the field).
api_key.expires_atstring (date-time) | nullnoInstant from which the key stops authenticating; null = never. A rotation sets it on the old key to the end of the overlap.
api_key.created_by_user_idstring (uuid) | nullnoThe dashboard user whose session minted the key. null when another dd_ key, the operator or the seeds did, and on every key that predates the field.
api_key.created_atstring (date-time)no

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 — ApiKeyAdminForbidden ​

Minting or revoking an API key was refused. code role_forbidden: the caller is a dashboard login without the account_admin role, or a dd_ key in a tenant that already has an active account_admin (founder knob api_key_manages_people, default off); the per-tenant knob api_key_admin_only (default on) turns this off.

json
{
  "error": {
    "code": "role_forbidden",
    "message": "minting or revoking an API key takes a dashboard login with the account_admin role; a dd_ key may do it only while the tenant has no account_admin"
  }
}

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"
  }
}

422 — Unprocessable ​

Shape is fine, semantics are not (broken domain rule).

json
{
  "error": {
    "code": "unprocessable",
    "message": "scheduled_at is closer than the minimum notice for a callback (10 minutes unless the tenant's callback_min_notice_seconds says otherwise)"
  }
}

429 — ApiKeyRateLimited ​

This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (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"
  }
}

Revoke an API key ​

http
DELETE /v1/api_keys/{id}

Sets revoked_at; keys are never deleted. Idempotent revocation semantics.

Role rules: revoking takes an account_admin login (403 role_forbidden for a member AND, once the tenant has an active account_admin, for a dd_ key; per-tenant knob api_key_admin_only, default on; founder knob api_key_manages_people, default off). A tenant with no active account_admin keeps the dd_ key revoking. Every revocation is written to the key audit trail. A key that holds a permission list may revoke only itself and keys whose permissions it covers: otherwise 403 permission_escalation.

A test API key may act only on test keys: against a live key it is refused with 403 test_environment and the key is left untouched.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200 ​

The revoked key.

FieldTypeRequiredDescription
api_keyobject (ApiKey)yesKey metadata — the token hash is never exposed.
api_key.idstring (uuid)no
api_key.labelstringno
api_key.environmentlive · testnolive (every key that predates the field) or test: a test key reaches no outbound path. Fixed at creation.
api_key.revoked_atstring (date-time) | nullno
api_key.last_used_atstring (date-time) | nullnoRotation-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.
api_key.allowed_cidrsarray of stringnoNetwork allowlist. [] = unrestricted. Outside it a request is served and counted, or refused with 401, per the tenant knob api_key_network_enforced (default off).
api_key.stalebooleannotrue 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; a key expires on its own only when it has an expires_at.
api_key.permissionsarray of ApiKeyPermission | nullnoRoute groups the key may enter. null = all (every key that predates the field).
api_key.expires_atstring (date-time) | nullnoInstant from which the key stops authenticating; null = never. A rotation sets it on the old key to the end of the overlap.
api_key.created_by_user_idstring (uuid) | nullnoThe dashboard user whose session minted the key. null when another dd_ key, the operator or the seeds did, and on every key that predates the field.
api_key.created_atstring (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 — ApiKeyAdminForbidden ​

Minting or revoking an API key was refused. code role_forbidden: the caller is a dashboard login without the account_admin role, or a dd_ key in a tenant that already has an active account_admin (founder knob api_key_manages_people, default off); the per-tenant knob api_key_admin_only (default on) turns this off.

json
{
  "error": {
    "code": "role_forbidden",
    "message": "minting or revoking an API key takes a dashboard login with the account_admin role; a dd_ key may do it only while the tenant has no account_admin"
  }
}

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 (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"
  }
}

Rotate an API key in one step, with an overlap ​

http
POST /v1/api_keys/{id}/rotate

Mints a new key with the same label, environment, network list and permissions and leaves the old one authenticating for overlap_seconds (default 3600, at most 86400; 0 cuts it at once); after that only the new key does. The old key's expires_at becomes the end of the overlap (never later than one it already had) and it is returned as rotated. The new token is shown ONCE. The key audit trail gets issued (new) and rotated (old). Same roles, mint rate limit and live-key limit as minting; during the overlap both keys count toward the limit. A key that holds a permission list rotates only keys whose permissions it covers (403 permission_escalation). A revoked, expired or unknown key is 404.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON)

FieldTypeRequiredDescription
overlap_secondsintegerno
expires_atstring (date-time) | nullnoOptional expiry for the NEW key (the old key's expiry is not inherited).
json
{
  "overlap_seconds": 1800
}

Responses

201 ​

The new key (with its token, once) and the old one with its overlap end.

FieldTypeRequiredDescription
api_keyobject (ApiKey)yesKey metadata — the token hash is never exposed.
api_key.idstring (uuid)no
api_key.labelstringno
api_key.environmentlive · testnolive (every key that predates the field) or test: a test key reaches no outbound path. Fixed at creation.
api_key.revoked_atstring (date-time) | nullno
api_key.last_used_atstring (date-time) | nullnoRotation-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.
api_key.allowed_cidrsarray of stringnoNetwork allowlist. [] = unrestricted. Outside it a request is served and counted, or refused with 401, per the tenant knob api_key_network_enforced (default off).
api_key.stalebooleannotrue 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; a key expires on its own only when it has an expires_at.
api_key.permissionsarray of ApiKeyPermission | nullnoRoute groups the key may enter. null = all (every key that predates the field).
api_key.expires_atstring (date-time) | nullnoInstant from which the key stops authenticating; null = never. A rotation sets it on the old key to the end of the overlap.
api_key.created_by_user_idstring (uuid) | nullnoThe dashboard user whose session minted the key. null when another dd_ key, the operator or the seeds did, and on every key that predates the field.
api_key.created_atstring (date-time)no
tokenstringyesPlaintext bearer token, shown exactly once.
rotatedobject (ApiKey)yesKey metadata — the token hash is never exposed.
rotated.idstring (uuid)no
rotated.labelstringno
rotated.environmentlive · testnolive (every key that predates the field) or test: a test key reaches no outbound path. Fixed at creation.
rotated.revoked_atstring (date-time) | nullno
rotated.last_used_atstring (date-time) | nullnoRotation-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.
rotated.allowed_cidrsarray of stringnoNetwork allowlist. [] = unrestricted. Outside it a request is served and counted, or refused with 401, per the tenant knob api_key_network_enforced (default off).
rotated.stalebooleannotrue 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; a key expires on its own only when it has an expires_at.
rotated.permissionsarray of ApiKeyPermission | nullnoRoute groups the key may enter. null = all (every key that predates the field).
rotated.expires_atstring (date-time) | nullnoInstant from which the key stops authenticating; null = never. A rotation sets it on the old key to the end of the overlap.
rotated.created_by_user_idstring (uuid) | nullnoThe dashboard user whose session minted the key. null when another dd_ key, the operator or the seeds did, and on every key that predates the field.
rotated.created_atstring (date-time)no

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 — ApiKeyAdminForbidden ​

Minting or revoking an API key was refused. code role_forbidden: the caller is a dashboard login without the account_admin role, or a dd_ key in a tenant that already has an active account_admin (founder knob api_key_manages_people, default off); the per-tenant knob api_key_admin_only (default on) turns this off.

json
{
  "error": {
    "code": "role_forbidden",
    "message": "minting or revoking an API key takes a dashboard login with the account_admin role; a dd_ key may do it only while the tenant has no account_admin"
  }
}

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 — ApiKeyLimit ​

The tenant already holds the maximum number of LIVE API keys (per-tenant knob api_keys_max_live, default 50). code api_key_limit. Nothing was minted; revoke a key (DELETE /v1/api_keys/{id}) and retry.

json
{
  "error": {
    "code": "api_key_limit",
    "message": "this tenant already holds the maximum number of live API keys; revoke one first"
  }
}

422 — Unprocessable ​

Shape is fine, semantics are not (broken domain rule).

json
{
  "error": {
    "code": "unprocessable",
    "message": "scheduled_at is closer than the minimum notice for a callback (10 minutes unless the tenant's callback_min_notice_seconds says otherwise)"
  }
}

429 — ApiKeyRateLimited ​

This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (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"
  }
}

Requests made with one API key ​

http
GET /v1/api_keys/{id}/requests

The request trail of ONE dd_ key, newest first, keyset-paginated: method, route TEMPLATE (/v1/calls/:call_id, never the URL with ids), HTTP status, duration and the request_id the caller also saw in x-request-id. A request shows up within seconds (the writer is asynchronous, off the request path).

Never stored: request or response bodies, headers (Authorization included), the query string, the IP or the key hash. Rows are kept API_REQUEST_LOG_RETENTION_DAYS days (default 30, minimum 1). A 429 from the key quota is a row like any other; it carries the route template the router had matched (/v1 when none yet).

Filters: since (inclusive) and until (exclusive), RFC-3339; status, one exact HTTP status. A malformed filter is 400, never a silently empty page. Tenant scope (dd_ key or dashboard login); a key of another tenant is 404.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.
sincequerystring (date-time)no
untilquerystring (date-time)no
statusqueryintegerno
limitqueryintegerno
cursorquerystringnoOpaque keyset cursor — the next_cursor of the previous page.

Responses

200 ​

One page of requests, newest first.

FieldTypeRequiredDescription
requestsarray of ApiRequestyes
requests[].idstring (uuid)yes
requests[].request_idstringyesThe x-request-id of the request.
requests[].methodstringyes
requests[].routestringyesRoute template, never the URL with ids.
requests[].statusintegeryes
requests[].duration_usintegeryesMicroseconds from the key being accepted to the response leaving.
requests[].requested_atstring (date-time)yes
next_cursorstring | nullyesCursor of the next page; null on the last one.

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 (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. 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 (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"
  }
}

Requests per minute of one API key, with its 429s ​

http
GET /v1/api_keys/{id}/requests/summary

Counts per minute of the requests of ONE key: requests, rate_limited (status 429, the key or tenant quota) and server_errors (status >= 500). Only minutes with traffic appear, newest first. Window [since, until): by default the last hour, at most 1440 minutes (400 beyond that). Same scope, retention and 404 rule as the list.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.
sincequerystring (date-time)no
untilquerystring (date-time)no

Responses

200 ​

The per-minute counts.

FieldTypeRequiredDescription
minutesarray of ApiRequestMinuteyes
minutes[].minutestring (date-time)yes
minutes[].requestsintegeryes
minutes[].rate_limitedintegeryesResponses with status 429.
minutes[].server_errorsintegeryesResponses with status >= 500.

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 (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. 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 (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"
  }
}

Nothing in these docs is legal advice — always confirm compliance posture with your own counsel.