Appearance
Widgets
Generated from
DialerDigital/corecommit0fc18008b3c8— specopenapi/dialer-v1.yamlv0.134.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.
Embeddable agent-widget lifecycle — ddw_ browser tokens + the origin allowlist.
Endpoints
| Method | Path | Summary |
|---|---|---|
GET | /v1/widget_tokens | List widget tokens |
POST | /v1/widget_tokens | Mint a widget token |
DELETE | /v1/widget_tokens/{id} | Revoke a widget token |
POST | /v1/widget_tokens/renew | Renew the caller's own widget session |
POST | /v1/widget_tokens/introspect | The caller's own widget session context |
POST | /v1/widget_tokens/sip_credential | Mint the caller's own ephemeral SIP credential |
DELETE | /v1/widget_tokens/sip_credential | Hand back the caller's own ephemeral SIP credential |
GET | /v1/widget_origins | List allowlisted origins |
POST | /v1/widget_origins | Add an allowlisted origin |
DELETE | /v1/widget_origins/{id} | Disable an allowlisted origin |
POST | /v1/widget_origins/{id}/enable | Re-enable an allowlisted origin |
List widget tokens
http
GET /v1/widget_tokensIds, agent, labels, expiry and revocation only — the token hash is never exposed.
Responses
200
The tenant's widget tokens.
| Field | Type | Required | Description |
|---|---|---|---|
widget_tokens | array of WidgetToken | yes | |
widget_tokens[].id | string (uuid) | no | |
widget_tokens[].agent_id | string (uuid) | no | |
widget_tokens[].label | string | no | |
widget_tokens[].allowed_origin_id | string (uuid) | null | no | Optional pin to one widget_origins row — narrows the EMBED origins surfaced to the frame (CSP/postMessage) to just that one; null = all the tenant's enabled embed origins. NOT the auth boundary (that is the widget-host Origin). Agent-login mints UNPINNED. |
widget_tokens[].expires_at | string (date-time) | no | |
widget_tokens[].session_expires_at | string (date-time) | no | |
widget_tokens[].revoked_at | string (date-time) | null | no | |
widget_tokens[].last_used_at | string (date-time) | null | no | Rotation-hygiene clock, same meaning as ApiKey.last_used_at: the last time this token resolved SUCCESSFULLY (every gate cleared), written at most once a minute per token, so a fresh reading may lag by up to that much. null means it has not resolved successfully since the column acquired a hot-path writer — not that the token was never presented. Renewal also stamps it. |
widget_tokens[].created_at | string (date-time) | no | |
widget_tokens[].updated_at | string (date-time) | no | Dates the ROW, not the mint: the use clock above and the lifecycle writes (renew, teardown) all move it. |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Mint a widget token
http
POST /v1/widget_tokensMints a ddw_ browser token bound to one agent. The plaintext is returned ONCE; only its sha256 is stored. TTLs are server-authoritative (never client input). Rate-limited per tenant.
WHO the token is minted for depends on the principal (ADR #133, ADR #134). A dd_ machine key names any agent of its tenant and agent_id is required for it (400 when missing). A ddu_ login session whose person is linked to an agent seat mints for THAT seat: agent_id may be omitted (filled with the seat) or repeat it; naming another agent is 403 forbidden and no token is minted — a person cannot coin another agent's credential. A ddu_ without a linked seat may mint for another agent only with users.role = account_admin — a member is 403 role_forbidden — once the tenant's catalog key ddu_author_role_enforced is on (born off: until then the seatless session keeps the dd_ contract, agent_id required).
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
agent_id | string (uuid) | no | The agent this token acts as. REQUIRED for a dd_ key; optional (and, if present, must be the caller's own seat) for a ddu_ session linked to a seat. |
label | string | no | Free-form label (default ""). |
allowed_origin_id | string (uuid) | no | Optional — pin to a single widget_origins row (narrows the EMBED origins surfaced to the frame; NOT the auth boundary, which is the widget-host Origin). Agent-login mints UNPINNED. |
json
{
"agent_id": "7a3f0f7e-52a1-4f3d-9f9d-27e5a1b9c001",
"label": "crm-embed"
}Responses
201
Token minted; token is the only time the plaintext is visible.
| Field | Type | Required | Description |
|---|---|---|---|
widget_token | object (WidgetToken) | yes | A ddw_ browser widget token — the plaintext + hash are never exposed here. |
widget_token.id | string (uuid) | no | |
widget_token.agent_id | string (uuid) | no | |
widget_token.label | string | no | |
widget_token.allowed_origin_id | string (uuid) | null | no | Optional pin to one widget_origins row — narrows the EMBED origins surfaced to the frame (CSP/postMessage) to just that one; null = all the tenant's enabled embed origins. NOT the auth boundary (that is the widget-host Origin). Agent-login mints UNPINNED. |
widget_token.expires_at | string (date-time) | no | |
widget_token.session_expires_at | string (date-time) | no | |
widget_token.revoked_at | string (date-time) | null | no | |
widget_token.last_used_at | string (date-time) | null | no | Rotation-hygiene clock, same meaning as ApiKey.last_used_at: the last time this token resolved SUCCESSFULLY (every gate cleared), written at most once a minute per token, so a fresh reading may lag by up to that much. null means it has not resolved successfully since the column acquired a hot-path writer — not that the token was never presented. Renewal also stamps it. |
widget_token.created_at | string (date-time) | no | |
widget_token.updated_at | string (date-time) | no | Dates the ROW, not the mint: the use clock above and the lifecycle writes (renew, teardown) all move it. |
token | string | yes | Plaintext ddw_ token, shown exactly 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 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
json
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}429 — WidgetRateLimited
The widget-token write rate brake refused this request (retryable later). code rate_limited. mint is bounded per tenant; renew per token. Retry-After is the time left in the fixed window.
json
{
"error": {
"code": "rate_limited",
"message": "widget token mint rate limit exceeded; retry later"
}
}Revoke a widget token
http
DELETE /v1/widget_tokens/{id}Sets revoked_at; tokens are never deleted. Idempotent.
ALSO tears the agent's SIP endpoint down (NR-02): deletes the ephemeral agentcred credential and the live usrloc binding. Without it a revoked token left the softphone able to REGISTER and carry calls until the credential's 24h autoexpire. The DB revocation is durable and happens regardless; sip_teardown reports whether the SIP half succeeded, so a 200 never implies an eviction that did not happen.
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 token, plus the outcome of the SIP teardown.
| Field | Type | Required | Description |
|---|---|---|---|
widget_token | object (WidgetToken) | yes | A ddw_ browser widget token — the plaintext + hash are never exposed here. |
widget_token.id | string (uuid) | no | |
widget_token.agent_id | string (uuid) | no | |
widget_token.label | string | no | |
widget_token.allowed_origin_id | string (uuid) | null | no | Optional pin to one widget_origins row — narrows the EMBED origins surfaced to the frame (CSP/postMessage) to just that one; null = all the tenant's enabled embed origins. NOT the auth boundary (that is the widget-host Origin). Agent-login mints UNPINNED. |
widget_token.expires_at | string (date-time) | no | |
widget_token.session_expires_at | string (date-time) | no | |
widget_token.revoked_at | string (date-time) | null | no | |
widget_token.last_used_at | string (date-time) | null | no | Rotation-hygiene clock, same meaning as ApiKey.last_used_at: the last time this token resolved SUCCESSFULLY (every gate cleared), written at most once a minute per token, so a fresh reading may lag by up to that much. null means it has not resolved successfully since the column acquired a hot-path writer — not that the token was never presented. Renewal also stamps it. |
widget_token.created_at | string (date-time) | no | |
widget_token.updated_at | string (date-time) | no | Dates the ROW, not the mint: the use clock above and the lifecycle writes (renew, teardown) all move it. |
sip_teardown | ok · skipped · failed | yes | Outcome of the SIP teardown that accompanies a revocation (NR-02): the deletion of the agent's ephemeral agentcred credential and its live usrloc binding in Kamailio. * ok — credential and binding removed; the softphone can no longer register. * skipped — nothing to remove: the agent has no SIP extension, SBC provisioning is not configured in this environment, or the agent still holds a VIGENT session (see below) so this revocation left no access open. * failed — the DB revocation stands, but the SIP credential is STILL LIVE and will remain so until its 24h autoexpire. Treat as an open access path: retry, or evict the agent by other means. It is a field and not an error status on purpose: the revocation itself is durable and did happen, so a 5xx would be a lie in the other direction. What must never happen is a silent 200 implying an eviction that did not occur. The teardown follows the TRANSITION, not the agent. Revoking a token id that was already revoked — an id a client can legitimately hold, since GET /v1/widget_tokens lists revoked rows, and since minting supersedes the previous token — must NOT evict, because the agent's SUCCESSOR session would be the one cut. The teardown therefore runs only when the agent is left with no vigent token; otherwise it reports skipped. This is what keeps failed's retry advice usable: an already-revoked token with no live successor still evicts on a retried DELETE, so retrying is a real remedy and not a no-op. |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Renew the caller's own widget session
http
POST /v1/widget_tokens/renewSlides the AUTHENTICATED ddw_ token's expiry forward (never past the session cap). Authenticated by the ddw_ itself; operates ONLY on the caller's own token (no id from path/body). Rate-limited per token.
Responses
200
The renewed token.
| Field | Type | Required | Description |
|---|---|---|---|
widget_token | object (WidgetToken) | yes | A ddw_ browser widget token — the plaintext + hash are never exposed here. |
widget_token.id | string (uuid) | no | |
widget_token.agent_id | string (uuid) | no | |
widget_token.label | string | no | |
widget_token.allowed_origin_id | string (uuid) | null | no | Optional pin to one widget_origins row — narrows the EMBED origins surfaced to the frame (CSP/postMessage) to just that one; null = all the tenant's enabled embed origins. NOT the auth boundary (that is the widget-host Origin). Agent-login mints UNPINNED. |
widget_token.expires_at | string (date-time) | no | |
widget_token.session_expires_at | string (date-time) | no | |
widget_token.revoked_at | string (date-time) | null | no | |
widget_token.last_used_at | string (date-time) | null | no | Rotation-hygiene clock, same meaning as ApiKey.last_used_at: the last time this token resolved SUCCESSFULLY (every gate cleared), written at most once a minute per token, so a fresh reading may lag by up to that much. null means it has not resolved successfully since the column acquired a hot-path writer — not that the token was never presented. Renewal also stamps it. |
widget_token.created_at | string (date-time) | no | |
widget_token.updated_at | string (date-time) | no | Dates the ROW, not the mint: the use clock above and the lifecycle writes (renew, teardown) all move it. |
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 — WidgetRateLimited
The widget-token write rate brake refused this request (retryable later). code rate_limited. mint is bounded per tenant; renew per token. Retry-After is the time left in the fixed window.
json
{
"error": {
"code": "rate_limited",
"message": "widget token mint rate limit exceeded; retry later"
}
}The caller's own widget session context
http
POST /v1/widget_tokens/introspectReturns the authenticated ddw_ session's tenant/agent, its allowlisted origins (for the frame CSP) and its session clocks. Authenticated by the ddw_ itself; tenant-scoped, no enumeration.
Responses
200
The caller's live session context.
| Field | Type | Required | Description |
|---|---|---|---|
widget_session | object (WidgetSession) | yes | The introspection view a frame boots from (its own live session). |
widget_session.tenant_id | string (uuid) | yes | |
widget_session.agent_id | string (uuid) | yes | |
widget_session.origins | array of string | yes | The session's enabled EMBED origins (widget_origins) for the frame's CSP / postMessage — NOT the auth boundary (the widget-host Origin). |
widget_session.expires_at | string (date-time) | yes | |
widget_session.session_expires_at | string (date-time) | yes |
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"
}
}Mint the caller's own ephemeral SIP credential
http
POST /v1/widget_tokens/sip_credentialProvisions the EPHEMERAL SIP credential (agentcred_w) of the AUTHENTICATED ddw_ session and returns it once, in memory — the frame's bootstrap for registering over WSS (MT-CTI-07 fase 2).
Authenticated by the ddw_ itself and operating ONLY on the caller's own session: the token id comes from the bearer, never from a path or body value, so one session can neither mint nor read another's. The credential is per TOKEN (w-<token_id>@t-<tenant>.<base>), never per agent extension, and lives in its own htable and its own usrloc AOR — revoking it cannot reach the agent's softphone credential, nor the other way round.
FAIL-CLOSED: if the credential cannot be issued, NOTHING is returned. There is no fallback to agentcred, the agent's 24 h softphone credential; a frame with no audio is the intended outcome.
Idempotent per session: a re-issue OVERWRITES the key, so the PREVIOUS password of that same session stops working. Rate-limited per token.
The password is returned EXACTLY ONCE and is stored nowhere on the server side — not in Postgres, not in a log line, not in an audit row. The audit trail records THAT a credential was issued, never WHAT.
Responses
200
The bootstrap, or sip: null when the widget SIP lane is not armed in this environment (WIDGET_SIP_ENABLED=false, or no SBC configured). null is a DEPLOYMENT state, not a failure of the caller: no credential is invented and the frame simply has no audio. Branch on it rather than retrying.
| Field | Type | Required | Description |
|---|---|---|---|
sip | WidgetSipCredential | null | yes |
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 — WidgetRateLimited
The widget-token write rate brake refused this request (retryable later). code rate_limited. mint is bounded per tenant; renew per token. Retry-After is the time left in the fixed window.
json
{
"error": {
"code": "rate_limited",
"message": "widget token mint rate limit exceeded; retry later"
}
}503 — WidgetSipUnavailable
The SIP credential could not be provisioned (code sbc_unavailable), so NOTHING was returned. FAIL-CLOSED and retryable: there is no fallback to the agent's long-lived agentcred, because handing the frame a credential the server did not intend to grant is worse than a frame with no audio.
json
{
"error": {
"code": "sbc_unavailable",
"message": "widget sip provisioning unavailable, retry"
}
}Hand back the caller's own ephemeral SIP credential
http
DELETE /v1/widget_tokens/sip_credentialDeletes the agentcred_w credential of the AUTHENTICATED ddw_ session and its usrloc binding, WITHOUT ending the session: the token stays live and may mint again. What the frame calls on unload, on logout, or when the agent moves to a deskphone.
Its reason to exist is the residue: without it the only teardown for a session that stops using audio is the htable autoexpire or revoking the whole token — up to 12 h of a live credential nobody is going to use.
Idempotent, and never an error to call for a session that never lit SIP up. Scoped to the caller's own session (the token id comes from the bearer), so it can never reach a sibling session's credential, nor the agent's agentcred.
Responses
200
The teardown outcome. failed is reported in a 200 ON PURPOSE (ADR #56 D3: an SBC failure is said, not eaten): the frame's session does not depend on the teardown having succeeded, and what is left over is settled anyway by the htable autoexpire or the session-cap sweeper. Do not retry in a loop on it.
| Field | Type | Required | Description |
|---|---|---|---|
sip_teardown | ok · skipped · failed | yes | Outcome of tearing down a WIDGET session's ephemeral credential — the agentcred_w lane. Distinct from SipTeardown, which reports on the agent's agentcred: the two lanes have separate htables and separate AORs precisely so that one can never reach the other, and reporting them with one schema is how they start being read as one. * ok — credential and usrloc binding removed; this frame can no longer register. * skipped — nothing to remove: the widget SIP lane is not armed in this environment, so no credential was ever issued. * failed — the SBC RPC failed and the credential stays live until the agentcred_w autoexpire. Logged and emitted as telemetry rather than swallowed. Reported inside a 200 on purpose; do not retry in a loop. |
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"
}
}List allowlisted origins
http
GET /v1/widget_originsResponses
200
The tenant's allowlisted origins (enabled + disabled).
| Field | Type | Required | Description |
|---|---|---|---|
widget_origins | array of WidgetOrigin | yes | |
widget_origins[].id | string (uuid) | no | |
widget_origins[].origin | string | no | Canonical https origin (lowercase host, default :443 stripped). |
widget_origins[].label | string | no | |
widget_origins[].disabled_at | string (date-time) | null | no | Non-null = disabled (dropped from the embed allowlist — no frame-ancestors/postMessage, never surfaced to a frame). |
widget_origins[].created_at | string (date-time) | no | |
widget_origins[].updated_at | string (date-time) | no |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Add an allowlisted origin
http
POST /v1/widget_originsThe raw origin is normalized to canonical https form; a malformed value is a 400.
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
origin | string | yes | A web origin, e.g. https://crm.example.com. |
label | string | no |
json
{
"origin": "https://crm.example.com"
}Responses
201
The added origin (canonical form).
| Field | Type | Required | Description |
|---|---|---|---|
widget_origin | object (WidgetOrigin) | yes | A per-tenant EMBED-allowlist web origin (canonical https form) — governs who may frame the widget (CSP frame-ancestors / postMessage), NOT the auth boundary (the widget-host Origin). |
widget_origin.id | string (uuid) | no | |
widget_origin.origin | string | no | Canonical https origin (lowercase host, default :443 stripped). |
widget_origin.label | string | no | |
widget_origin.disabled_at | string (date-time) | null | no | Non-null = disabled (dropped from the embed allowlist — no frame-ancestors/postMessage, never surfaced to a frame). |
widget_origin.created_at | string (date-time) | no | |
widget_origin.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"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}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 (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Disable an allowlisted origin
http
DELETE /v1/widget_origins/{id}Soft-disable (dropped from the embed allowlist — stops framing this CRM via frame-ancestors/postMessage; does NOT block agent login, which gates on the widget-host Origin). Origins are never deleted.
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 origin.
| Field | Type | Required | Description |
|---|---|---|---|
widget_origin | object (WidgetOrigin) | yes | A per-tenant EMBED-allowlist web origin (canonical https form) — governs who may frame the widget (CSP frame-ancestors / postMessage), NOT the auth boundary (the widget-host Origin). |
widget_origin.id | string (uuid) | no | |
widget_origin.origin | string | no | Canonical https origin (lowercase host, default :443 stripped). |
widget_origin.label | string | no | |
widget_origin.disabled_at | string (date-time) | null | no | Non-null = disabled (dropped from the embed allowlist — no frame-ancestors/postMessage, never surfaced to a frame). |
widget_origin.created_at | string (date-time) | no | |
widget_origin.updated_at | string (date-time) | no |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Re-enable an allowlisted origin
http
POST /v1/widget_origins/{id}/enableParameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The re-enabled origin.
| Field | Type | Required | Description |
|---|---|---|---|
widget_origin | object (WidgetOrigin) | yes | A per-tenant EMBED-allowlist web origin (canonical https form) — governs who may frame the widget (CSP frame-ancestors / postMessage), NOT the auth boundary (the widget-host Origin). |
widget_origin.id | string (uuid) | no | |
widget_origin.origin | string | no | Canonical https origin (lowercase host, default :443 stripped). |
widget_origin.label | string | no | |
widget_origin.disabled_at | string (date-time) | null | no | Non-null = disabled (dropped from the embed allowlist — no frame-ancestors/postMessage, never surfaced to a frame). |
widget_origin.created_at | string (date-time) | no | |
widget_origin.updated_at | string (date-time) | no |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}