Appearance
API keys
Generated from
DialerDigital/corecommitbbb8e373d900— specopenapi/dialer-v1.yamlv0.151.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.
Per-tenant API keys (sha256 at rest; plaintext shown once).
Endpoints
| Method | Path | Summary |
|---|---|---|
GET | /v1/api_keys | List API keys |
POST | /v1/api_keys | Mint 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}/rotate | Rotate an API key in one step, with an overlap |
GET | /v1/api_keys/{id}/requests | Requests made with one API key |
GET | /v1/api_keys/{id}/requests/summary | Requests per minute of one API key, with its 429s |
List API keys
http
GET /v1/api_keysIds, labels and revocation timestamps only — hashes are never exposed.
Responses
200
The tenant's keys.
| Field | Type | Required | Description |
|---|---|---|---|
api_keys | array of ApiKey | yes | |
api_keys[].id | string (uuid) | no | |
api_keys[].label | string | no | |
api_keys[].environment | live · test | no | live (every key that predates the field) or test: a test key reaches no outbound path. Fixed at creation. |
api_keys[].revoked_at | string (date-time) | null | no | |
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. |
api_keys[].allowed_cidrs | array of string | no | Network 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[].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; a key expires on its own only when it has an expires_at. |
api_keys[].permissions | array of ApiKeyPermission | null | no | Route groups the key may enter. null = all (every key that predates the field). |
api_keys[].expires_at | string (date-time) | null | no | Instant 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_id | string (uuid) | null | no | The 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_at | string (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_keysThe 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)
| Field | Type | Required | Description |
|---|---|---|---|
label | string | no | Free-form label (default ""). |
environment | live · test | no | test mints a key that never reaches calls, SMS or email (dd_test_ prefix). Immutable. Any other value is 422. |
allowed_cidrs | array of string | no | CIDRs (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. |
permissions | array of ApiKeyPermission | null | no | Route groups the key may enter. Absent or null = all. [] = none. An unknown group is 422. |
expires_at | string (date-time) | null | no | Instant 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.
| Field | Type | Required | Description |
|---|---|---|---|
api_key | object (ApiKey) | yes | Key metadata — the token hash is never exposed. |
api_key.id | string (uuid) | no | |
api_key.label | string | no | |
api_key.environment | live · test | no | live (every key that predates the field) or test: a test key reaches no outbound path. Fixed at creation. |
api_key.revoked_at | string (date-time) | null | no | |
api_key.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. |
api_key.allowed_cidrs | array of string | no | Network 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.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; a key expires on its own only when it has an expires_at. |
api_key.permissions | array of ApiKeyPermission | null | no | Route groups the key may enter. null = all (every key that predates the field). |
api_key.expires_at | string (date-time) | null | no | Instant 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_id | string (uuid) | null | no | The 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_at | string (date-time) | no | |
token | string | yes | Plaintext 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
| 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 |
|---|---|---|---|
allowed_cidrs | array of string | yes |
json
{
"allowed_cidrs": [
"203.0.113.0/24",
"2001:db8::/32"
]
}Responses
200
The key with its new list.
| Field | Type | Required | Description |
|---|---|---|---|
api_key | object (ApiKey) | yes | Key metadata — the token hash is never exposed. |
api_key.id | string (uuid) | no | |
api_key.label | string | no | |
api_key.environment | live · test | no | live (every key that predates the field) or test: a test key reaches no outbound path. Fixed at creation. |
api_key.revoked_at | string (date-time) | null | no | |
api_key.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. |
api_key.allowed_cidrs | array of string | no | Network 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.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; a key expires on its own only when it has an expires_at. |
api_key.permissions | array of ApiKeyPermission | null | no | Route groups the key may enter. null = all (every key that predates the field). |
api_key.expires_at | string (date-time) | null | no | Instant 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_id | string (uuid) | null | no | The 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_at | string (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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The revoked key.
| Field | Type | Required | Description |
|---|---|---|---|
api_key | object (ApiKey) | yes | Key metadata — the token hash is never exposed. |
api_key.id | string (uuid) | no | |
api_key.label | string | no | |
api_key.environment | live · test | no | live (every key that predates the field) or test: a test key reaches no outbound path. Fixed at creation. |
api_key.revoked_at | string (date-time) | null | no | |
api_key.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. |
api_key.allowed_cidrs | array of string | no | Network 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.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; a key expires on its own only when it has an expires_at. |
api_key.permissions | array of ApiKeyPermission | null | no | Route groups the key may enter. null = all (every key that predates the field). |
api_key.expires_at | string (date-time) | null | no | Instant 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_id | string (uuid) | null | no | The 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_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 — 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}/rotateMints 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
overlap_seconds | integer | no | |
expires_at | string (date-time) | null | no | Optional 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.
| Field | Type | Required | Description |
|---|---|---|---|
api_key | object (ApiKey) | yes | Key metadata — the token hash is never exposed. |
api_key.id | string (uuid) | no | |
api_key.label | string | no | |
api_key.environment | live · test | no | live (every key that predates the field) or test: a test key reaches no outbound path. Fixed at creation. |
api_key.revoked_at | string (date-time) | null | no | |
api_key.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. |
api_key.allowed_cidrs | array of string | no | Network 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.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; a key expires on its own only when it has an expires_at. |
api_key.permissions | array of ApiKeyPermission | null | no | Route groups the key may enter. null = all (every key that predates the field). |
api_key.expires_at | string (date-time) | null | no | Instant 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_id | string (uuid) | null | no | The 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_at | string (date-time) | no | |
token | string | yes | Plaintext bearer token, shown exactly once. |
rotated | object (ApiKey) | yes | Key metadata — the token hash is never exposed. |
rotated.id | string (uuid) | no | |
rotated.label | string | no | |
rotated.environment | live · test | no | live (every key that predates the field) or test: a test key reaches no outbound path. Fixed at creation. |
rotated.revoked_at | string (date-time) | null | no | |
rotated.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. |
rotated.allowed_cidrs | array of string | no | Network 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.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; a key expires on its own only when it has an expires_at. |
rotated.permissions | array of ApiKeyPermission | null | no | Route groups the key may enter. null = all (every key that predates the field). |
rotated.expires_at | string (date-time) | null | no | Instant 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_id | string (uuid) | null | no | The 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_at | string (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}/requestsThe 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
since | query | string (date-time) | no | |
until | query | string (date-time) | no | |
status | query | integer | no | |
limit | query | integer | no | |
cursor | query | string | no | Opaque keyset cursor — the next_cursor of the previous page. |
Responses
200
One page of requests, newest first.
| Field | Type | Required | Description |
|---|---|---|---|
requests | array of ApiRequest | yes | |
requests[].id | string (uuid) | yes | |
requests[].request_id | string | yes | The x-request-id of the request. |
requests[].method | string | yes | |
requests[].route | string | yes | Route template, never the URL with ids. |
requests[].status | integer | yes | |
requests[].duration_us | integer | yes | Microseconds from the key being accepted to the response leaving. |
requests[].requested_at | string (date-time) | yes | |
next_cursor | string | null | yes | Cursor 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/summaryCounts 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
since | query | string (date-time) | no | |
until | query | string (date-time) | no |
Responses
200
The per-minute counts.
| Field | Type | Required | Description |
|---|---|---|---|
minutes | array of ApiRequestMinute | yes | |
minutes[].minute | string (date-time) | yes | |
minutes[].requests | integer | yes | |
minutes[].rate_limited | integer | yes | Responses with status 429. |
minutes[].server_errors | integer | yes | Responses 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"
}
}