Appearance
Inbound routes
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.
Inbound DID routing + SBC provisioning.
Endpoints
| Method | Path | Summary |
|---|---|---|
GET | /v1/inbound-routes | List inbound routes |
POST | /v1/inbound-routes | Create an inbound route |
GET | /v1/inbound-routes/{id} | Fetch one inbound route |
PATCH | /v1/inbound-routes/{id} | Update an inbound route (e164 immutable) |
POST | /v1/inbound-routes/{id}/provision | Push the route to the SIP edge |
POST | /v1/inbound-routes/{id}/disable | Disable an inbound route |
List inbound routes
http
GET /v1/inbound-routesResponses
200
The tenant's inbound routes.
| Field | Type | Required | Description |
|---|---|---|---|
inbound_routes | array of InboundRoute | yes | |
inbound_routes[].id | string (uuid) | no | |
inbound_routes[].tenant_id | string (uuid) | no | |
inbound_routes[].e164 | string | no | Immutable after creation. |
inbound_routes[].dest_type | agent · queue · ivr · optout · ai_agent · webhook | no | ivr runs the inbound IVR menu dest_ref names. A route saved without a menu is still refused at connect with CALL_REJECTED. optout is the opt-out number: the calling number joins the tenant's own do-not-call list at connect and FreeSWITCH confirms and hangs up; such a route always reads dest_ref: null, recording: false, ai_enabled: false. agent bridges straight to the registered agent; queue anchors the leg on FreeSWITCH. Today the production FreeSWITCH image ships no queue extensions (the park pen and dd_queue_hold are a pending freeswitch dependency, not yet delivered), so a queue leg is hung up by FreeSWITCH as UNALLOCATED_NUMBER regardless of the flag. Once they ship: with the tenant's inbound_queue_stream_enabled flag ON and its agent_session_journal_enabled ON too (with the seat journal OFF the call is refused unanswered, since no agent could be offered it), core admits the call to the route's queue (enqueued), the extension answers with a generated tone, the owner offers it to the oldest Ready agent and bridges it; flag OFF (default): the leg ends unanswered in the pen. ai_agent names a voice AI agent of the account: ai_agent_id (a live agent of this account) and queue_id (the active queue the call falls back to) are REQUIRED, or the write is 400 invalid; ai_enabled reads true and recording false, both derived from the type, not chosen. The admission is decided before the call is answered: with the tenant's inbound_ai_enabled flag off (the default) or any of its checks failing, the call is treated as a call to the queue_id queue and is not answered by the AI; the reason is recorded in the call's CDR evidence (compliance_snapshot.inbound_ai). Nothing hands a call to the agent yet (the fork is a later delivery), so a call to this number is not answered by the AI. webhook hands the call to the developer's webhook: webhook_url is REQUIRED (https on 443, a host that resolves only to public addresses, or 400 invalid) and the route is bare — recording: true, dest_ref, queue_id, ai_agent_id and ai_enabled: true in the same write are 400 invalid; left out, recording is false. The call passes the same plan, spend and capacity verdicts as any inbound call BEFORE the webhook is ever contacted; the leg then waits in FreeSWITCH's dd_ivr_hold, which answers it, and from the answer core POSTs each step to webhook_url (call.started, then gather.completed or gather.timeout) signed with X-DD-Signature: t=<unix>,v1=<hex(HMAC-SHA256(secret, "<t>.<body>"))> — the same construction as an outbound-webhook endpoint's. The webhook answers 200 with {"actions": [...]}: 1 to 8 actions among say (text, up to 500 bytes, rendered to audio by the platform), gather (num_digits 1-10, timeout_seconds 1-30, finish_on_key; ends the list, its result is the NEXT request) and the terminal verbs queue (queue_id), agent (agent_id) and hangup; the list must end in a gather or a terminal verb. Fail-closed: no answer in 3 seconds, a non-2xx, a body that is not valid actions, a destination that is not this account's, or more than 20 requests per call hang the call up (NORMAL_CLEARING); nothing is ever routed to a human or a queue the webhook did not choose. Every step is recorded in an append-only, signed stream. A route saved without its URL is refused at connect (refused_webhook_unavailable). A webhook route does not record. |
inbound_routes[].dest_ref | string | null | no | |
inbound_routes[].queue_id | string,null (uuid) | no | The queue of a queue route; null on any other type, and on a queue route saved without one. |
inbound_routes[].ai_agent_id | string,null (uuid) | no | The AI agent of an ai_agent route; null on any other type. |
inbound_routes[].hours | InboundRouteHours | null | no | The route's hours, or null when it answers at all hours. A closed message reads back as the text that was sent. |
inbound_routes[].webhook_url | string,null | no | The developer's webhook of a webhook route; null on any other type. |
inbound_routes[].webhook_secret_hint | string,null | no | The last 4 characters of a webhook route's signing secret; null on any other type. The secret itself is never readable. |
inbound_routes[].webhook_secret | string | no | The signing secret of a webhook route (whsec_…), present ONLY in the response of the write that minted it (create, a write that moves the route to webhook, a write that names webhook_url); never on a read or a list. Store it: it cannot be shown again. |
inbound_routes[].recording | boolean | no | |
inbound_routes[].ai_enabled | boolean | no | true if and only if dest_type is ai_agent (derived); false on every other type. A write that names the opposite is 400. Routes that held true before the flag was refused were turned off by a data migration. |
inbound_routes[].status | active · disabled | no | |
inbound_routes[].notes | string | null | no | |
inbound_routes[].created_at | string (date-time) | no | |
inbound_routes[].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"
}
}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"
}
}Create an inbound route
http
POST /v1/inbound-routese164 is immutable after creation; tenant_id is injected by the router.
Role rules: a dd_ key or an account_admin login; a member login is 403 role_forbidden (per-tenant knob telephony_config_admin_only, default on). The GET of this resource is not affected.
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
e164 | string | yes | E.164 (immutable after creation). It must be a number of THIS account (one registered with /v1/dids, or a house number assigned to it): a number that is another account's or that nobody registered is 422 unprocessable (e164 not a number of this account). A PATCH that brings a route back to active is judged the same way; a route that already exists is never re-judged. |
dest_type | agent · queue · ivr · optout · ai_agent · webhook | yes | ivr runs one of the tenant's inbound IVR menus: dest_ref is REQUIRED and must be the id of a menu published with /v1/inbound-ivr-menus in this account and not retired, or the write is 400 invalid. Each call hears the menu's version current when it connects; FreeSWITCH answers it after the plan verdict, core plays the prompts, and the key (or, after two silent windows, the menu's default) sends it to a queue, an agent, the opt-out or a hangup. optout is the opt-out number (47 CFR 64.1200(b)(3)): the calling number is added to the tenant's own do-not-call list when the call connects, before anything answers and whatever the tenant's plan; with an active plan FreeSWITCH then plays a platform-composed confirmation naming the tenant's seller (or a tone) and hangs up, with a dark plan the call is refused unanswered. A caller without a usable number (hidden or invalid) is refused and nothing is recorded. An optout route is bare: a non-null dest_ref, recording: true or ai_enabled: true in the same write are refused with 400 invalid; left out, they are set to none/false. agent bridges straight to the registered agent; queue anchors the leg on FreeSWITCH. Today the production FreeSWITCH image ships no queue extensions (the park pen and dd_queue_hold are a pending freeswitch dependency, not yet delivered), so a queue leg is hung up by FreeSWITCH as UNALLOCATED_NUMBER regardless of the flag. Once they ship: with the tenant's inbound_queue_stream_enabled flag ON and its agent_session_journal_enabled ON too (with the seat journal OFF the call is refused unanswered, since no agent could be offered it), core admits the call to the route's queue (enqueued), the extension answers with a generated tone, the owner offers it to the oldest Ready agent and bridges it; flag OFF (default): the leg ends unanswered in the pen. ai_agent names a voice AI agent of the account: ai_agent_id (a live agent of this account) and queue_id (the active queue the call falls back to) are REQUIRED, or the write is 400 invalid; ai_enabled reads true and recording false, both derived from the type, not chosen. The admission is decided before the call is answered: with the tenant's inbound_ai_enabled flag off (the default) or any of its checks failing, the call is treated as a call to the queue_id queue and is not answered by the AI; the reason is recorded in the call's CDR evidence (compliance_snapshot.inbound_ai). Nothing hands a call to the agent yet (the fork is a later delivery), so a call to this number is not answered by the AI. webhook hands the call to the developer's webhook: webhook_url is REQUIRED (https on 443, a host that resolves only to public addresses, or 400 invalid) and the route is bare — recording: true, dest_ref, queue_id, ai_agent_id and ai_enabled: true in the same write are 400 invalid; left out, recording is false. The call passes the same plan, spend and capacity verdicts as any inbound call BEFORE the webhook is ever contacted; the leg then waits in FreeSWITCH's dd_ivr_hold, which answers it, and from the answer core POSTs each step to webhook_url (call.started, then gather.completed or gather.timeout) signed with X-DD-Signature: t=<unix>,v1=<hex(HMAC-SHA256(secret, "<t>.<body>"))> — the same construction as an outbound-webhook endpoint's. The webhook answers 200 with {"actions": [...]}: 1 to 8 actions among say (text, up to 500 bytes, rendered to audio by the platform), gather (num_digits 1-10, timeout_seconds 1-30, finish_on_key; ends the list, its result is the NEXT request) and the terminal verbs queue (queue_id), agent (agent_id) and hangup; the list must end in a gather or a terminal verb. Fail-closed: no answer in 3 seconds, a non-2xx, a body that is not valid actions, a destination that is not this account's, or more than 20 requests per call hang the call up (NORMAL_CLEARING); nothing is ever routed to a human or a queue the webhook did not choose. Every step is recorded in an append-only, signed stream. A route saved without its URL is refused at connect (refused_webhook_unavailable). A webhook route does not record. |
dest_ref | string (uuid) | no | Required for agent (the agent id) and for ivr (the id of a published, non-retired inbound IVR menu of this account, else 400 invalid); a non-null value is refused on an optout route. A value that is not a canonical UUID is refused with 400 invalid (dest_ref must be a UUID). |
queue_id | string (uuid) | no | The queue of a queue route or the fallback queue of an ai_agent route: an ACTIVE queue of this account created with /v1/inbound-queues; a retired one, another account's, one that does not exist or a malformed id is 400 invalid. REQUIRED on an ai_agent route: the queue the call falls back to, held to the same rule. Refused on any other dest_type. Optional on a queue route: without it a queue route is accepted, as before, and refuses its calls at connect. A PATCH that moves the route to another type without naming it drops it. |
ai_agent_id | string (uuid) | no | REQUIRED on an ai_agent route: the id of a live AI agent of this account, created with /v1/ai/agents; a draft, paused or retired one, another account's, one that does not exist or a malformed id is 400 invalid. Refused on any other dest_type; a PATCH that moves the route to another type without naming it drops it. The agent is judged AT WRITE (a PATCH that points the route at an agent or brings it back to active): pausing it later does not touch the route, and retiring it while an active route names it is 409 on PATCH /v1/ai/agents/{id} and DELETE /v1/ai/agents/{id}. |
hours | InboundRouteHours | null | no | The route's hours. Absent or null = the number answers at all hours, exactly as before; on a PATCH, null REMOVES the hours and leaving the key out keeps them. Outside the hours, or on a holiday, a NEW call is treated as the route closed names (a message or another destination); a call already admitted is never cut when the closing time passes. An optout route has no hours: sending them with dest_type: optout, or moving a route that has them to optout without hours: null in the same write, is 422. A zone that does not resolve, a malformed or repeated holiday, more than 366 holidays, a closed that is not exactly one of its two shapes, or one that names a queue, menu, agent or AI agent that is not live in this account is 422 unprocessable. The closed message is rendered to audio when the hours are saved; if it cannot be, the write is 503 prompt_render_failed and nothing is saved. A route with hours is provisioned anchored on FreeSWITCH (an agent route without recording is otherwise delivered straight to the agent and core never sees the call): after writing the hours, POST /v1/inbound-routes/{id}/provision so the SIP edge learns it. |
webhook_url | string (uri) | no | REQUIRED on a webhook route: the developer's https URL on port 443 whose host resolves only to public addresses (no loopback, private, link-local or metadata range), or 400 invalid; refused on any other dest_type (a PATCH that moves the route off webhook without naming it drops it). The route's signing secret (whsec_…) is minted when the route is created as webhook, when a write moves it to webhook and whenever a write NAMES webhook_url (a new URL is a new receiver, it never inherits the old key); it is returned ONCE, as webhook_secret in that write's response, and afterwards only webhook_secret_hint is readable. |
recording | boolean | no | true is refused on an optout route, on a webhook route and on an ai_agent route (the AI leg is not recorded); omitted there, it is false. Default true on every other route. |
ai_enabled | boolean | no | Derived from the destination, not chosen: true if and only if dest_type is ai_agent. Naming true on any other destination, or false on an ai_agent route, is 400 invalid (ai_enabled is derived from the destination...); omit it, or send the value the destination implies (default false). |
notes | string | no |
json
{
"e164": "+13125550171",
"dest_type": "queue"
}Responses
201
Created route.
| Field | Type | Required | Description |
|---|---|---|---|
inbound_route | object (InboundRoute) | yes | |
inbound_route.id | string (uuid) | no | |
inbound_route.tenant_id | string (uuid) | no | |
inbound_route.e164 | string | no | Immutable after creation. |
inbound_route.dest_type | agent · queue · ivr · optout · ai_agent · webhook | no | ivr runs the inbound IVR menu dest_ref names. A route saved without a menu is still refused at connect with CALL_REJECTED. optout is the opt-out number: the calling number joins the tenant's own do-not-call list at connect and FreeSWITCH confirms and hangs up; such a route always reads dest_ref: null, recording: false, ai_enabled: false. agent bridges straight to the registered agent; queue anchors the leg on FreeSWITCH. Today the production FreeSWITCH image ships no queue extensions (the park pen and dd_queue_hold are a pending freeswitch dependency, not yet delivered), so a queue leg is hung up by FreeSWITCH as UNALLOCATED_NUMBER regardless of the flag. Once they ship: with the tenant's inbound_queue_stream_enabled flag ON and its agent_session_journal_enabled ON too (with the seat journal OFF the call is refused unanswered, since no agent could be offered it), core admits the call to the route's queue (enqueued), the extension answers with a generated tone, the owner offers it to the oldest Ready agent and bridges it; flag OFF (default): the leg ends unanswered in the pen. ai_agent names a voice AI agent of the account: ai_agent_id (a live agent of this account) and queue_id (the active queue the call falls back to) are REQUIRED, or the write is 400 invalid; ai_enabled reads true and recording false, both derived from the type, not chosen. The admission is decided before the call is answered: with the tenant's inbound_ai_enabled flag off (the default) or any of its checks failing, the call is treated as a call to the queue_id queue and is not answered by the AI; the reason is recorded in the call's CDR evidence (compliance_snapshot.inbound_ai). Nothing hands a call to the agent yet (the fork is a later delivery), so a call to this number is not answered by the AI. webhook hands the call to the developer's webhook: webhook_url is REQUIRED (https on 443, a host that resolves only to public addresses, or 400 invalid) and the route is bare — recording: true, dest_ref, queue_id, ai_agent_id and ai_enabled: true in the same write are 400 invalid; left out, recording is false. The call passes the same plan, spend and capacity verdicts as any inbound call BEFORE the webhook is ever contacted; the leg then waits in FreeSWITCH's dd_ivr_hold, which answers it, and from the answer core POSTs each step to webhook_url (call.started, then gather.completed or gather.timeout) signed with X-DD-Signature: t=<unix>,v1=<hex(HMAC-SHA256(secret, "<t>.<body>"))> — the same construction as an outbound-webhook endpoint's. The webhook answers 200 with {"actions": [...]}: 1 to 8 actions among say (text, up to 500 bytes, rendered to audio by the platform), gather (num_digits 1-10, timeout_seconds 1-30, finish_on_key; ends the list, its result is the NEXT request) and the terminal verbs queue (queue_id), agent (agent_id) and hangup; the list must end in a gather or a terminal verb. Fail-closed: no answer in 3 seconds, a non-2xx, a body that is not valid actions, a destination that is not this account's, or more than 20 requests per call hang the call up (NORMAL_CLEARING); nothing is ever routed to a human or a queue the webhook did not choose. Every step is recorded in an append-only, signed stream. A route saved without its URL is refused at connect (refused_webhook_unavailable). A webhook route does not record. |
inbound_route.dest_ref | string | null | no | |
inbound_route.queue_id | string,null (uuid) | no | The queue of a queue route; null on any other type, and on a queue route saved without one. |
inbound_route.ai_agent_id | string,null (uuid) | no | The AI agent of an ai_agent route; null on any other type. |
inbound_route.hours | InboundRouteHours | null | no | The route's hours, or null when it answers at all hours. A closed message reads back as the text that was sent. |
inbound_route.webhook_url | string,null | no | The developer's webhook of a webhook route; null on any other type. |
inbound_route.webhook_secret_hint | string,null | no | The last 4 characters of a webhook route's signing secret; null on any other type. The secret itself is never readable. |
inbound_route.webhook_secret | string | no | The signing secret of a webhook route (whsec_…), present ONLY in the response of the write that minted it (create, a write that moves the route to webhook, a write that names webhook_url); never on a read or a list. Store it: it cannot be shown again. |
inbound_route.recording | boolean | no | |
inbound_route.ai_enabled | boolean | no | true if and only if dest_type is ai_agent (derived); false on every other type. A write that names the opposite is 400. Routes that held true before the flag was refused were turned off by a data migration. |
inbound_route.status | active · disabled | no | |
inbound_route.notes | string | null | no | |
inbound_route.created_at | string (date-time) | no | |
inbound_route.updated_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"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
json
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}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"
}
}503
The closed message of the hours could not be rendered to audio (prompt_render_failed); the route was not saved. The request is right and may succeed once the text-to-speech answers.
Fetch one inbound route
http
GET /v1/inbound-routes/{id}Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The route.
| Field | Type | Required | Description |
|---|---|---|---|
inbound_route | object (InboundRoute) | yes | |
inbound_route.id | string (uuid) | no | |
inbound_route.tenant_id | string (uuid) | no | |
inbound_route.e164 | string | no | Immutable after creation. |
inbound_route.dest_type | agent · queue · ivr · optout · ai_agent · webhook | no | ivr runs the inbound IVR menu dest_ref names. A route saved without a menu is still refused at connect with CALL_REJECTED. optout is the opt-out number: the calling number joins the tenant's own do-not-call list at connect and FreeSWITCH confirms and hangs up; such a route always reads dest_ref: null, recording: false, ai_enabled: false. agent bridges straight to the registered agent; queue anchors the leg on FreeSWITCH. Today the production FreeSWITCH image ships no queue extensions (the park pen and dd_queue_hold are a pending freeswitch dependency, not yet delivered), so a queue leg is hung up by FreeSWITCH as UNALLOCATED_NUMBER regardless of the flag. Once they ship: with the tenant's inbound_queue_stream_enabled flag ON and its agent_session_journal_enabled ON too (with the seat journal OFF the call is refused unanswered, since no agent could be offered it), core admits the call to the route's queue (enqueued), the extension answers with a generated tone, the owner offers it to the oldest Ready agent and bridges it; flag OFF (default): the leg ends unanswered in the pen. ai_agent names a voice AI agent of the account: ai_agent_id (a live agent of this account) and queue_id (the active queue the call falls back to) are REQUIRED, or the write is 400 invalid; ai_enabled reads true and recording false, both derived from the type, not chosen. The admission is decided before the call is answered: with the tenant's inbound_ai_enabled flag off (the default) or any of its checks failing, the call is treated as a call to the queue_id queue and is not answered by the AI; the reason is recorded in the call's CDR evidence (compliance_snapshot.inbound_ai). Nothing hands a call to the agent yet (the fork is a later delivery), so a call to this number is not answered by the AI. webhook hands the call to the developer's webhook: webhook_url is REQUIRED (https on 443, a host that resolves only to public addresses, or 400 invalid) and the route is bare — recording: true, dest_ref, queue_id, ai_agent_id and ai_enabled: true in the same write are 400 invalid; left out, recording is false. The call passes the same plan, spend and capacity verdicts as any inbound call BEFORE the webhook is ever contacted; the leg then waits in FreeSWITCH's dd_ivr_hold, which answers it, and from the answer core POSTs each step to webhook_url (call.started, then gather.completed or gather.timeout) signed with X-DD-Signature: t=<unix>,v1=<hex(HMAC-SHA256(secret, "<t>.<body>"))> — the same construction as an outbound-webhook endpoint's. The webhook answers 200 with {"actions": [...]}: 1 to 8 actions among say (text, up to 500 bytes, rendered to audio by the platform), gather (num_digits 1-10, timeout_seconds 1-30, finish_on_key; ends the list, its result is the NEXT request) and the terminal verbs queue (queue_id), agent (agent_id) and hangup; the list must end in a gather or a terminal verb. Fail-closed: no answer in 3 seconds, a non-2xx, a body that is not valid actions, a destination that is not this account's, or more than 20 requests per call hang the call up (NORMAL_CLEARING); nothing is ever routed to a human or a queue the webhook did not choose. Every step is recorded in an append-only, signed stream. A route saved without its URL is refused at connect (refused_webhook_unavailable). A webhook route does not record. |
inbound_route.dest_ref | string | null | no | |
inbound_route.queue_id | string,null (uuid) | no | The queue of a queue route; null on any other type, and on a queue route saved without one. |
inbound_route.ai_agent_id | string,null (uuid) | no | The AI agent of an ai_agent route; null on any other type. |
inbound_route.hours | InboundRouteHours | null | no | The route's hours, or null when it answers at all hours. A closed message reads back as the text that was sent. |
inbound_route.webhook_url | string,null | no | The developer's webhook of a webhook route; null on any other type. |
inbound_route.webhook_secret_hint | string,null | no | The last 4 characters of a webhook route's signing secret; null on any other type. The secret itself is never readable. |
inbound_route.webhook_secret | string | no | The signing secret of a webhook route (whsec_…), present ONLY in the response of the write that minted it (create, a write that moves the route to webhook, a write that names webhook_url); never on a read or a list. Store it: it cannot be shown again. |
inbound_route.recording | boolean | no | |
inbound_route.ai_enabled | boolean | no | true if and only if dest_type is ai_agent (derived); false on every other type. A write that names the opposite is 400. Routes that held true before the flag was refused were turned off by a data migration. |
inbound_route.status | active · disabled | no | |
inbound_route.notes | string | null | no | |
inbound_route.created_at | string (date-time) | no | |
inbound_route.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"
}
}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"
}
}Update an inbound route (e164 immutable)
http
PATCH /v1/inbound-routes/{id}Role rules: a dd_ key or an account_admin login; a member login is 403 role_forbidden (per-tenant knob telephony_config_admin_only, default on). The GET of this resource is not affected.
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 |
|---|---|---|---|
dest_type | agent · queue · ivr · optout · ai_agent · webhook | no | ivr runs one of the tenant's inbound IVR menus: dest_ref is REQUIRED and must be the id of a menu published with /v1/inbound-ivr-menus in this account and not retired, or the write is 400 invalid. Each call hears the menu's version current when it connects; FreeSWITCH answers it after the plan verdict, core plays the prompts, and the key (or, after two silent windows, the menu's default) sends it to a queue, an agent, the opt-out or a hangup. optout is the opt-out number (47 CFR 64.1200(b)(3)): the calling number is added to the tenant's own do-not-call list when the call connects, before anything answers and whatever the tenant's plan; with an active plan FreeSWITCH then plays a platform-composed confirmation naming the tenant's seller (or a tone) and hangs up, with a dark plan the call is refused unanswered. A caller without a usable number (hidden or invalid) is refused and nothing is recorded. An optout route is bare: a non-null dest_ref, recording: true or ai_enabled: true in the same write are refused with 400 invalid; left out, they are set to none/false. agent bridges straight to the registered agent; queue anchors the leg on FreeSWITCH. Today the production FreeSWITCH image ships no queue extensions (the park pen and dd_queue_hold are a pending freeswitch dependency, not yet delivered), so a queue leg is hung up by FreeSWITCH as UNALLOCATED_NUMBER regardless of the flag. Once they ship: with the tenant's inbound_queue_stream_enabled flag ON and its agent_session_journal_enabled ON too (with the seat journal OFF the call is refused unanswered, since no agent could be offered it), core admits the call to the route's queue (enqueued), the extension answers with a generated tone, the owner offers it to the oldest Ready agent and bridges it; flag OFF (default): the leg ends unanswered in the pen. ai_agent names a voice AI agent of the account: ai_agent_id (a live agent of this account) and queue_id (the active queue the call falls back to) are REQUIRED, or the write is 400 invalid; ai_enabled reads true and recording false, both derived from the type, not chosen. The admission is decided before the call is answered: with the tenant's inbound_ai_enabled flag off (the default) or any of its checks failing, the call is treated as a call to the queue_id queue and is not answered by the AI; the reason is recorded in the call's CDR evidence (compliance_snapshot.inbound_ai). Nothing hands a call to the agent yet (the fork is a later delivery), so a call to this number is not answered by the AI. webhook hands the call to the developer's webhook: webhook_url is REQUIRED (https on 443, a host that resolves only to public addresses, or 400 invalid) and the route is bare — recording: true, dest_ref, queue_id, ai_agent_id and ai_enabled: true in the same write are 400 invalid; left out, recording is false. The call passes the same plan, spend and capacity verdicts as any inbound call BEFORE the webhook is ever contacted; the leg then waits in FreeSWITCH's dd_ivr_hold, which answers it, and from the answer core POSTs each step to webhook_url (call.started, then gather.completed or gather.timeout) signed with X-DD-Signature: t=<unix>,v1=<hex(HMAC-SHA256(secret, "<t>.<body>"))> — the same construction as an outbound-webhook endpoint's. The webhook answers 200 with {"actions": [...]}: 1 to 8 actions among say (text, up to 500 bytes, rendered to audio by the platform), gather (num_digits 1-10, timeout_seconds 1-30, finish_on_key; ends the list, its result is the NEXT request) and the terminal verbs queue (queue_id), agent (agent_id) and hangup; the list must end in a gather or a terminal verb. Fail-closed: no answer in 3 seconds, a non-2xx, a body that is not valid actions, a destination that is not this account's, or more than 20 requests per call hang the call up (NORMAL_CLEARING); nothing is ever routed to a human or a queue the webhook did not choose. Every step is recorded in an append-only, signed stream. A route saved without its URL is refused at connect (refused_webhook_unavailable). A webhook route does not record. |
dest_ref | string (uuid) | no | Required for agent (the agent id) and for ivr (the id of a published, non-retired inbound IVR menu of this account, else 400 invalid); a non-null value is refused on an optout route. A value that is not a canonical UUID is refused with 400 invalid (dest_ref must be a UUID). |
queue_id | string (uuid) | no | The queue of a queue route or the fallback queue of an ai_agent route: an ACTIVE queue of this account created with /v1/inbound-queues; a retired one, another account's, one that does not exist or a malformed id is 400 invalid. REQUIRED on an ai_agent route: the queue the call falls back to, held to the same rule. Refused on any other dest_type. Optional on a queue route: without it a queue route is accepted, as before, and refuses its calls at connect. A PATCH that moves the route to another type without naming it drops it. |
ai_agent_id | string (uuid) | no | REQUIRED on an ai_agent route: the id of a live AI agent of this account, created with /v1/ai/agents; a draft, paused or retired one, another account's, one that does not exist or a malformed id is 400 invalid. Refused on any other dest_type; a PATCH that moves the route to another type without naming it drops it. The agent is judged AT WRITE (a PATCH that points the route at an agent or brings it back to active): pausing it later does not touch the route, and retiring it while an active route names it is 409 on PATCH /v1/ai/agents/{id} and DELETE /v1/ai/agents/{id}. |
hours | InboundRouteHours | null | no | The route's hours. Absent or null = the number answers at all hours, exactly as before; on a PATCH, null REMOVES the hours and leaving the key out keeps them. Outside the hours, or on a holiday, a NEW call is treated as the route closed names (a message or another destination); a call already admitted is never cut when the closing time passes. An optout route has no hours: sending them with dest_type: optout, or moving a route that has them to optout without hours: null in the same write, is 422. A zone that does not resolve, a malformed or repeated holiday, more than 366 holidays, a closed that is not exactly one of its two shapes, or one that names a queue, menu, agent or AI agent that is not live in this account is 422 unprocessable. The closed message is rendered to audio when the hours are saved; if it cannot be, the write is 503 prompt_render_failed and nothing is saved. A route with hours is provisioned anchored on FreeSWITCH (an agent route without recording is otherwise delivered straight to the agent and core never sees the call): after writing the hours, POST /v1/inbound-routes/{id}/provision so the SIP edge learns it. |
webhook_url | string (uri) | no | REQUIRED on a webhook route: the developer's https URL on port 443 whose host resolves only to public addresses (no loopback, private, link-local or metadata range), or 400 invalid; refused on any other dest_type (a PATCH that moves the route off webhook without naming it drops it). The route's signing secret (whsec_…) is minted when the route is created as webhook, when a write moves it to webhook and whenever a write NAMES webhook_url (a new URL is a new receiver, it never inherits the old key); it is returned ONCE, as webhook_secret in that write's response, and afterwards only webhook_secret_hint is readable. |
recording | boolean | no | true is refused on an optout route, on a webhook route and on an ai_agent route (the AI leg is not recorded); omitted there, it is false. Default true on every other route. |
ai_enabled | boolean | no | Derived from the destination, not chosen: true if and only if dest_type is ai_agent. Naming true on any other destination, or false on an ai_agent route, is 400 invalid (ai_enabled is derived from the destination...); omit it, or send the value the destination implies (default false). |
status | active · disabled | no | |
notes | string | no |
json
{
"recording": false
}Responses
200
Updated route.
| Field | Type | Required | Description |
|---|---|---|---|
inbound_route | object (InboundRoute) | yes | |
inbound_route.id | string (uuid) | no | |
inbound_route.tenant_id | string (uuid) | no | |
inbound_route.e164 | string | no | Immutable after creation. |
inbound_route.dest_type | agent · queue · ivr · optout · ai_agent · webhook | no | ivr runs the inbound IVR menu dest_ref names. A route saved without a menu is still refused at connect with CALL_REJECTED. optout is the opt-out number: the calling number joins the tenant's own do-not-call list at connect and FreeSWITCH confirms and hangs up; such a route always reads dest_ref: null, recording: false, ai_enabled: false. agent bridges straight to the registered agent; queue anchors the leg on FreeSWITCH. Today the production FreeSWITCH image ships no queue extensions (the park pen and dd_queue_hold are a pending freeswitch dependency, not yet delivered), so a queue leg is hung up by FreeSWITCH as UNALLOCATED_NUMBER regardless of the flag. Once they ship: with the tenant's inbound_queue_stream_enabled flag ON and its agent_session_journal_enabled ON too (with the seat journal OFF the call is refused unanswered, since no agent could be offered it), core admits the call to the route's queue (enqueued), the extension answers with a generated tone, the owner offers it to the oldest Ready agent and bridges it; flag OFF (default): the leg ends unanswered in the pen. ai_agent names a voice AI agent of the account: ai_agent_id (a live agent of this account) and queue_id (the active queue the call falls back to) are REQUIRED, or the write is 400 invalid; ai_enabled reads true and recording false, both derived from the type, not chosen. The admission is decided before the call is answered: with the tenant's inbound_ai_enabled flag off (the default) or any of its checks failing, the call is treated as a call to the queue_id queue and is not answered by the AI; the reason is recorded in the call's CDR evidence (compliance_snapshot.inbound_ai). Nothing hands a call to the agent yet (the fork is a later delivery), so a call to this number is not answered by the AI. webhook hands the call to the developer's webhook: webhook_url is REQUIRED (https on 443, a host that resolves only to public addresses, or 400 invalid) and the route is bare — recording: true, dest_ref, queue_id, ai_agent_id and ai_enabled: true in the same write are 400 invalid; left out, recording is false. The call passes the same plan, spend and capacity verdicts as any inbound call BEFORE the webhook is ever contacted; the leg then waits in FreeSWITCH's dd_ivr_hold, which answers it, and from the answer core POSTs each step to webhook_url (call.started, then gather.completed or gather.timeout) signed with X-DD-Signature: t=<unix>,v1=<hex(HMAC-SHA256(secret, "<t>.<body>"))> — the same construction as an outbound-webhook endpoint's. The webhook answers 200 with {"actions": [...]}: 1 to 8 actions among say (text, up to 500 bytes, rendered to audio by the platform), gather (num_digits 1-10, timeout_seconds 1-30, finish_on_key; ends the list, its result is the NEXT request) and the terminal verbs queue (queue_id), agent (agent_id) and hangup; the list must end in a gather or a terminal verb. Fail-closed: no answer in 3 seconds, a non-2xx, a body that is not valid actions, a destination that is not this account's, or more than 20 requests per call hang the call up (NORMAL_CLEARING); nothing is ever routed to a human or a queue the webhook did not choose. Every step is recorded in an append-only, signed stream. A route saved without its URL is refused at connect (refused_webhook_unavailable). A webhook route does not record. |
inbound_route.dest_ref | string | null | no | |
inbound_route.queue_id | string,null (uuid) | no | The queue of a queue route; null on any other type, and on a queue route saved without one. |
inbound_route.ai_agent_id | string,null (uuid) | no | The AI agent of an ai_agent route; null on any other type. |
inbound_route.hours | InboundRouteHours | null | no | The route's hours, or null when it answers at all hours. A closed message reads back as the text that was sent. |
inbound_route.webhook_url | string,null | no | The developer's webhook of a webhook route; null on any other type. |
inbound_route.webhook_secret_hint | string,null | no | The last 4 characters of a webhook route's signing secret; null on any other type. The secret itself is never readable. |
inbound_route.webhook_secret | string | no | The signing secret of a webhook route (whsec_…), present ONLY in the response of the write that minted it (create, a write that moves the route to webhook, a write that names webhook_url); never on a read or a list. Store it: it cannot be shown again. |
inbound_route.recording | boolean | no | |
inbound_route.ai_enabled | boolean | no | true if and only if dest_type is ai_agent (derived); false on every other type. A write that names the opposite is 400. Routes that held true before the flag was refused were turned off by a data migration. |
inbound_route.status | active · disabled | no | |
inbound_route.notes | string | null | no | |
inbound_route.created_at | string (date-time) | no | |
inbound_route.updated_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"
}
}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"
}
}503
The closed message of the hours could not be rendered to audio (prompt_render_failed); the route was not saved. The request is right and may succeed once the text-to-speech answers.
Push the route to the SIP edge
http
POST /v1/inbound-routes/{id}/provision409 when the route is disabled; 422 when the destination agent cannot be resolved for this sede or has no SIP extension (seat) to deliver to; 200 {"status": "sbc_disabled"} when no KAMAILIO_RPC_URL is configured.
Role rules: a dd_ key or an account_admin login; a member login is 403 role_forbidden (per-tenant knob telephony_config_admin_only, default on). The GET of this resource is not affected.
A test API key is refused with 403 test_environment: it does not create, change, retire or provision the real trunks, providers or routes of the tenant.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
Provisioned (or SBC disabled in this environment).
| Field | Type | Required | Description |
|---|---|---|---|
status | provisioned · sbc_disabled | yes | sbc_disabled = no KAMAILIO_RPC_URL in this environment; the row's intent is recorded, not pushed. |
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"
}
}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 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
json
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}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"
}
}502 — SbcError
The SIP edge rejected the provisioning request.
json
{
"error": {
"code": "sbc_error",
"message": "the SIP edge rejected the provisioning request"
}
}Disable an inbound route
http
POST /v1/inbound-routes/{id}/disableRole rules: a dd_ key or an account_admin login; a member login is 403 role_forbidden (per-tenant knob telephony_config_admin_only, default on). The GET of this resource is not affected.
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 route.
| Field | Type | Required | Description |
|---|---|---|---|
inbound_route | object (InboundRoute) | yes | |
inbound_route.id | string (uuid) | no | |
inbound_route.tenant_id | string (uuid) | no | |
inbound_route.e164 | string | no | Immutable after creation. |
inbound_route.dest_type | agent · queue · ivr · optout · ai_agent · webhook | no | ivr runs the inbound IVR menu dest_ref names. A route saved without a menu is still refused at connect with CALL_REJECTED. optout is the opt-out number: the calling number joins the tenant's own do-not-call list at connect and FreeSWITCH confirms and hangs up; such a route always reads dest_ref: null, recording: false, ai_enabled: false. agent bridges straight to the registered agent; queue anchors the leg on FreeSWITCH. Today the production FreeSWITCH image ships no queue extensions (the park pen and dd_queue_hold are a pending freeswitch dependency, not yet delivered), so a queue leg is hung up by FreeSWITCH as UNALLOCATED_NUMBER regardless of the flag. Once they ship: with the tenant's inbound_queue_stream_enabled flag ON and its agent_session_journal_enabled ON too (with the seat journal OFF the call is refused unanswered, since no agent could be offered it), core admits the call to the route's queue (enqueued), the extension answers with a generated tone, the owner offers it to the oldest Ready agent and bridges it; flag OFF (default): the leg ends unanswered in the pen. ai_agent names a voice AI agent of the account: ai_agent_id (a live agent of this account) and queue_id (the active queue the call falls back to) are REQUIRED, or the write is 400 invalid; ai_enabled reads true and recording false, both derived from the type, not chosen. The admission is decided before the call is answered: with the tenant's inbound_ai_enabled flag off (the default) or any of its checks failing, the call is treated as a call to the queue_id queue and is not answered by the AI; the reason is recorded in the call's CDR evidence (compliance_snapshot.inbound_ai). Nothing hands a call to the agent yet (the fork is a later delivery), so a call to this number is not answered by the AI. webhook hands the call to the developer's webhook: webhook_url is REQUIRED (https on 443, a host that resolves only to public addresses, or 400 invalid) and the route is bare — recording: true, dest_ref, queue_id, ai_agent_id and ai_enabled: true in the same write are 400 invalid; left out, recording is false. The call passes the same plan, spend and capacity verdicts as any inbound call BEFORE the webhook is ever contacted; the leg then waits in FreeSWITCH's dd_ivr_hold, which answers it, and from the answer core POSTs each step to webhook_url (call.started, then gather.completed or gather.timeout) signed with X-DD-Signature: t=<unix>,v1=<hex(HMAC-SHA256(secret, "<t>.<body>"))> — the same construction as an outbound-webhook endpoint's. The webhook answers 200 with {"actions": [...]}: 1 to 8 actions among say (text, up to 500 bytes, rendered to audio by the platform), gather (num_digits 1-10, timeout_seconds 1-30, finish_on_key; ends the list, its result is the NEXT request) and the terminal verbs queue (queue_id), agent (agent_id) and hangup; the list must end in a gather or a terminal verb. Fail-closed: no answer in 3 seconds, a non-2xx, a body that is not valid actions, a destination that is not this account's, or more than 20 requests per call hang the call up (NORMAL_CLEARING); nothing is ever routed to a human or a queue the webhook did not choose. Every step is recorded in an append-only, signed stream. A route saved without its URL is refused at connect (refused_webhook_unavailable). A webhook route does not record. |
inbound_route.dest_ref | string | null | no | |
inbound_route.queue_id | string,null (uuid) | no | The queue of a queue route; null on any other type, and on a queue route saved without one. |
inbound_route.ai_agent_id | string,null (uuid) | no | The AI agent of an ai_agent route; null on any other type. |
inbound_route.hours | InboundRouteHours | null | no | The route's hours, or null when it answers at all hours. A closed message reads back as the text that was sent. |
inbound_route.webhook_url | string,null | no | The developer's webhook of a webhook route; null on any other type. |
inbound_route.webhook_secret_hint | string,null | no | The last 4 characters of a webhook route's signing secret; null on any other type. The secret itself is never readable. |
inbound_route.webhook_secret | string | no | The signing secret of a webhook route (whsec_…), present ONLY in the response of the write that minted it (create, a write that moves the route to webhook, a write that names webhook_url); never on a read or a list. Store it: it cannot be shown again. |
inbound_route.recording | boolean | no | |
inbound_route.ai_enabled | boolean | no | true if and only if dest_type is ai_agent (derived); false on every other type. A write that names the opposite is 400. Routes that held true before the flag was refused were turned off by a data migration. |
inbound_route.status | active · disabled | no | |
inbound_route.notes | string | null | no | |
inbound_route.created_at | string (date-time) | no | |
inbound_route.updated_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"
}
}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"
}
}