Appearance
Identity
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.
Authenticated tenant, account settings.
Endpoints
Authenticated tenant, user and derived SEAT
http
GET /v1/meReturns the tenant resolved from the bearer. When the bearer is a ddu_ login session (email+password dashboard auth), the response also carries the user behind the console; for a dd_ machine key the tenant-only shape is unchanged.
Also answers "which seat am I?" (seat + seat_source, core#1059, ADR #137). A client MUST NOT let the operator PICK a seat: since core#1034 (ADR #131) POST /v1/agents/checkin DERIVES the seat from the bearer and answers 403 seat_mismatch to any sip_extension in the body that does not match it. The seat here is derived by the same single derivation point the check-in and GET /v1/me/scorecard (ADR #132) use, so the two answers cannot disagree.
seat_source is ALWAYS present — a client never has to interpret a missing field:
token—seatis the caller's own agent row, in the same projectionGET /v1/agents/{id}returns.no_identity— add_machine key: no person behind it, so there is no "own" seat.seatisnull; the body is NOT consulted.seat_not_linked— addu_person with no active seat (e.g. a supervisor with no dialing position).seatisnull.seat_gone— the bearer points at a deleted agent row. Reachable only by theddw_lane, which this route denies before the handler (not on the widget allowlist); listed for completeness.
All four are 200. A person without a seat is a legitimate state and this route doubles as the SPA's bearer validator, so answering 403 there would break their login — that is the deliberate difference from GET /v1/me/scorecard, which IS 403 without a seat because there is no marker to show.
A dds_ support session (ADR #225) answers seat: null, seat_source: no_identity and a support block (read_only is always true, with the session's expires_at and the operator behind it) so a console can show that the session is read-only and when it ends.
Responses
200
The authenticated tenant, the user (ddu_) and the derived seat.
| Field | Type | Required | Description |
|---|---|---|---|
tenant | object (Tenant) | yes | |
tenant.id | string (uuid) | no | |
tenant.name | string | no | |
tenant.account_id | string (uuid) | no | The EMPRESA this tenant (sede) belongs to (DA-2). |
tenant.billing_account_id | string (uuid) | no | The billing account the sede is invoiced under (DA-3). Shared by every sede of a consolidated empresa; one per sede when per_site. |
tenant.status | active · suspended | no | |
tenant.synthetic | boolean | no | true for a demonstration tenant. It originates no traffic: manual dial, SMS send, the external leg of a transfer and the sandbox answer 403 synthetic_tenant. Always present, never null, so a console can warn BEFORE the action fails (core#2512). |
tenant.retention_months | integer | no | |
tenant.seats | integer | no | |
tenant.tier | starter · growth · scale · enterprise · dialer_core · compliance_pro · audit_shield | no | |
tenant.voice_ai_enabled | boolean | no | |
tenant.ai_monthly_budget_usd | number | null | no | Cost-integrity ceiling (admin-set; null = no cap). |
tenant.ai_max_call_seconds | integer | null | no | Per-AI-call hard duration ceiling (seconds) set by the admin. null means the PLATFORM default applies — never "no ceiling" (core#922, ADR #115). |
tenant.ai_max_call_seconds_effective | integer | no | The ceiling FreeSWITCH is actually handed for this tenant's AI legs: ai_max_call_seconds when set, else the cell's platform default (AI_MAX_CALL_SECONDS_DEFAULT, compiled fallback 900). Always present, never null. Read-only (derived). |
tenant.sms_monthly_budget_usd | number | null | no | Month-to-date SMS spend cap. |
tenant.human_initiated_enabled | boolean | no | D17 human-initiated consent posture (admin-set; default false). When true, a LIVE preview/manual/CTI voice dial with no artificial and no AI voice does not require prior express consent (no ATDS). Every other gate — DNC, quiet hours, Reg F, state matrix, artificial voice — still applies unchanged. |
tenant.max_cps | integer | null | no | Per-tenant CPS ceiling (admin-set; null = no ceiling). ADR #12. |
tenant.max_concurrent_channels | integer | null | no | Per-tenant ceiling of concurrent calls, one unit per call with or without an agent (admin-set; null = no ceiling). ADR #12, ADR #185. |
tenant.abandon_seller_name | string | null | no | Seller name for the FTC TSR abandoned-call identification message (admin-set; null = not configured, predictive over-dial stays dark). ADR #14. |
tenant.abandon_seller_phone | string | null | no | E.164 callback number for the abandoned-call identification message (admin-set; null = not configured). ADR #14. |
tenant.tax_country | string | null | no | ISO-3166-1 alpha-2 country of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77. |
tenant.tax_region | string | null | no | State/province code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77. |
tenant.tax_postal_code | string | null | no | Postal code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77. |
tenant.kyc_verified_at | string (date-time) | null | no | When the platform verified this customer (admin-set; null = not yet). A 1-seat account that is not verified cannot create or start a campaign that runs without an agent (403 verification_required, ADR #279). |
tenant.created_at | string (date-time) | no | |
tenant.updated_at | string (date-time) | no | |
user | object (User) | no | A dashboard user — the password hash is never exposed. |
user.id | string (uuid) | no | |
user.email | string (email) | no | Global login identifier, stored lowercase. |
user.name | string | no | |
user.status | active · disabled | no | |
user.role | member · account_admin | no | Intra-account privilege. Only an account_admin may create or modify the empresa-wide rows every sibling sede inherits (scope: "empresa" on carriers and SMS providers); a member gets 403. Read-only here: promotion is an operator gesture, never a request. |
user.password_updated_at | string (date-time) | no | |
user.last_login_at | string (date-time) | null | no | |
user.mfa_enabled | boolean | no | Whether the person has an ACTIVE second factor (an enrolment still pending confirmation is false). Emitted ONLY when the caller is an account_admin dashboard login — the one who can act on it with POST /v1/users/{id}/mfa/remove; for a member, a dd_ key, or when the caller's role cannot be determined the field is ABSENT (not false). Wherever a User is rendered, the same rule holds: GET /v1/users, GET /v1/me, the user returned by DELETE /v1/users/{id} and POST /v1/users/{id}/enable, and the user in the session responses (POST /v1/auth/login, POST /v1/auth/sso/exchange, and the two MFA completion routes) — there, for the account_admin who just signed in. |
user.created_at | string (date-time) | no | |
user.updated_at | string (date-time) | no | |
support | object | no | Only for a dds_ support session (ADR #225): the session is read-only and ends at expires_at. |
support.read_only | true | yes | |
support.session_id | string (uuid) | yes | |
support.expires_at | string (date-time) | yes | |
support.operator | string | yes | The platform operator who opened it, operator:<id> (<name>). |
seat | Agent | null | yes | The caller's own seat, or null when seat_source is not token. |
seat_source | token · no_identity · seat_not_linked · seat_gone | yes | How the seat was resolved (or why there is none). Always present. |
capabilities | object | yes | Per-tenant UI capabilities (core#1806), a closed list. A capability that cannot be resolved reads false. |
capabilities.conference | boolean | yes | true when the internal conference routes (POST /v1/calls/{call_id}/conference/add|remove) are enabled for this tenant; false means they answer 404. |
capabilities.email_channel | boolean | no | true when the tenant's email_channel_enabled flag is on (core#1925): the email sender and the email_blast runner may act. false (the default) means the channel is off: it writes no ledger row and calls no provider. It is NOT "ready to send": an active provider, the budget, the address provenance and every gate still apply, and core answers their typed errors. It is absent from a core that predates it, so a client MUST read a missing key as "not published yet" and never as false. |
screen_monitor | object (ScreenMonitorStatus) | yes | What GET /v1/me reports about screen monitoring for the caller's seat (core#1989). Fail-closed: a scope is authorized only when enabled is true AND scopes.<scope>.ack_current is true. enabled: false (the flag is off, or could not be resolved) reads every other field as empty. notice_ack_required is true whenever the function is enabled and the seat lacks an acknowledgement of the CURRENT version for ANY scope, also while no notice is published yet (then there is nothing to acknowledge: still closed). A caller with no seat (dd_ key, ddu_ without a seat, dds_) sees notice_ack_required: false and every ack_current: false. |
screen_monitor.enabled | boolean | yes | |
screen_monitor.notice_ack_required | boolean | yes | |
screen_monitor.notice_version | integer | null | yes | The current notice version, or null when none is published or the function is off. |
screen_monitor.scopes | object | yes | Per scope, whether the seat has an acknowledgement of the CURRENT version. An acknowledgement of one scope never makes the other current. |
screen_monitor.scopes.live_monitoring | object (ScreenMonitorScopeStatus) | yes | |
screen_monitor.scopes.live_monitoring.ack_current | boolean | yes | |
screen_monitor.scopes.live_monitoring.acked_at | string (date-time) | null | yes | When the seat acknowledged the current version for this scope, or null. |
screen_monitor.scopes.screen_recording | object (ScreenMonitorScopeStatus) | yes | |
screen_monitor.scopes.screen_recording.ack_current | boolean | yes | |
screen_monitor.scopes.screen_recording.acked_at | string (date-time) | null | yes | When the seat acknowledged the current version for this scope, or null. |
json
{
"tenant": {
"id": "0d4f4f9e-1f2a-4b53-9d3c-8a5e2f7b1c10",
"name": "Acme Collections",
"status": "active",
"synthetic": false,
"retention_months": 84,
"seats": 10,
"tier": "scale",
"voice_ai_enabled": true,
"ai_monthly_budget_usd": 500,
"ai_max_call_seconds": 600,
"ai_max_call_seconds_effective": 600,
"sms_monthly_budget_usd": 250,
"created_at": "2026-06-01T12:00:00Z",
"updated_at": "2026-06-20T09:30:00Z"
},
"seat": null,
"seat_source": "no_identity",
"capabilities": {
"conference": false,
"email_channel": false
},
"screen_monitor": {
"enabled": false,
"notice_ack_required": false,
"notice_version": null,
"scopes": {
"live_monitoring": {
"ack_current": false,
"acked_at": null
},
"screen_recording": {
"ack_current": false,
"acked_at": null
}
}
}
}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"
}
}The caller's OWN shift scorecard (seat derived from the token)
http
GET /v1/me/scorecardThe collector's personal marker: their calls in the window and the promises they took, for the seat the BEARER identifies. No parameter may name an agent — agent_id is refused by name with 400, never ignored, so a client can never believe it read a colleague's numbers.
Who gets a seat: a ddu_ dashboard login, through the users_agents.user_id link (403 seat_not_linked when the person has no active seat). A dd_ machine key has no person behind it and is 403. A ddw_ widget token is 403 before the handler — this route is not on the widget allowlist.
Window: half-open [from, to). Both are optional and to defaults to now. from defaults to the seat's ACTIVE check-in (window.source: checkin); when the seat has no active check-in — the normal state right after checkout, since checked_in_at is presence, not history — it defaults to now - 12h (window.source: fallback_12h). Twelve hours cover a whole collection shift without dragging in yesterday's. A client MUST surface source: a figure from an estimated window is shown as such, never as "the shift". calls counts ATTEMPTS, not CDR rows (the original plus its corrections are one call), and carries no ratios or minutes — derived occupancy is GET /v1/agents/occupancy, and two computations of the same thing is exactly what is avoided here.
Deliberately absent: the promise note (free text, encrypted at rest under GLBA), phone numbers, and commission.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string (date-time) | no | Inclusive start (RFC 3339). Defaults to the seat's active checked_in_at, or to now - 12h when there is none — see window.source. |
to | query | string (date-time) | no | Exclusive end (RFC 3339); must be after from. Defaults to now. |
Responses
200
The caller's own scorecard for the resolved window.
| Field | Type | Required | Description |
|---|---|---|---|
agent | object | yes | The seat DERIVED from the bearer — echoed so the client can show whose marker it is, never to let the client choose it. |
agent.id | string (uuid) | yes | |
agent.name | string | yes | |
agent.sip_extension | string | null | yes | |
window | object | yes | |
window.from | string (date-time) | yes | |
window.to | string (date-time) | yes | |
window.source | checkin · explicit · fallback_12h | yes | Where from came from: the seat's ACTIVE check-in (checkin, the shift), the client's own parameter (explicit), or the estimated now - 12h window used when the seat has no active check-in (fallback_12h, the normal state right after checkout). A client MUST show the estimated case as such. |
calls | object | yes | Counted per ATTEMPT (the original row plus every correction appended to it is ONE call), over attempts whose started_at is in the window and that are attributed to this seat — including the inbound attempt whose agent was named by a later assignment. Invariant: total == in_flight + sum(by_disposition). |
calls.total | integer | yes | |
calls.in_flight | integer | yes | Attempts with no settling correction yet. Counted apart instead of being given a fabricated disposition. |
calls.by_disposition | object | yes | Attempt counts keyed by the disposition of the LAST settling correction written for the attempt. |
promises | object | yes | Promises this seat RECORDED in the window (inserted_at, not promised_date: a shift contains promises dated weeks ahead). |
promises.count | integer | yes | |
promises.by_status | object | yes | |
promises.by_status.pending | integer | yes | |
promises.by_status.kept | integer | yes | |
promises.by_status.broken | integer | yes | |
promises.by_status.cancelled | integer | yes | |
promises.amount_cents | object | yes | Summed per currency — USD and MXN never sum together — and EXCLUDING cancelled promises: a cancellation is an operator withdrawal, so the row survives as evidence but the commitment does not. The cancelled ones are still visible in by_status. |
promises.amount_cents.USD | integer | yes | |
promises.amount_cents.MXN | integer | yes |
400
Malformed window, or an agent_id parameter — refused by name because this scorecard is always the caller's own seat.
json
{
"error": {
"code": "bad_request",
"message": "agent_id not accepted here: this scorecard is always the caller's own seat, derived from the token — use /v1/agents/occupancy or /v1/calls for another seat"
}
}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"
}
}The current screen-monitoring notice
http
GET /v1/screen-monitor/noticeThe CURRENT version of the notice a seat must acknowledge before its screen may be observed (core#1989, FASE 0 of dashboard#696). The notice is a global catalogue (no tenant) and append-only: publishing a newer version invalidates, without touching them, every acknowledgement of the older ones.
404 not_found when the tenant's founder-governed flag screen_monitor_enabled is off, and also when no version has been published yet. Reachable by a dd_ key and by a ddu_ login; a ddw_ widget token is 403 before the handler.
Responses
200
The current notice.
| Field | Type | Required | Description |
|---|---|---|---|
notice | object (ScreenMonitorNotice) | yes | One version of the screen-monitoring notice (GET /v1/screen-monitor/notice). Global, append-only. |
notice.version | integer | yes | |
notice.lang | string | yes | Language of the text, e.g. es. |
notice.title | string | yes | |
notice.body | string | yes | |
notice.text_sha256 | string | yes | SHA-256 (hex) of body in UTF-8; an acknowledgement copies it. |
notice.effective_at | string (date-time) | yes | |
notice.published_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"
}
}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"
}
}Acknowledgements of the tenant (supervisor/admin seat)
http
GET /v1/screen-monitor/acksThe tenant's acknowledgements, newest first, paginated by cursor. Allowed only to a seat whose template role is supervisor or admin (403 role_forbidden otherwise; a dd_ key and a ddu_ with no seat are 403). The role is evaluated always; it does not depend on supervision_role_enforced.
It does NOT consult screen_monitor_enabled: it is the record of what was accepted, and turning the function off neither deletes nor hides it. Nothing crosses tenants.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
limit | query | integer | no | Page size (a value over the ceiling is capped, not refused). |
cursor | query | string | no | Opaque keyset cursor — the next_cursor of the previous page. |
agent_id | query | string (uuid) | no | Only the acknowledgements of this seat. |
Responses
200
One page of acknowledgements, newest first.
| Field | Type | Required | Description |
|---|---|---|---|
acks | array of ScreenMonitorAck | yes | |
acks[].id | string (uuid) | yes | |
acks[].agent_id | string (uuid) | yes | |
acks[].notice_version | integer | yes | |
acks[].scope | live_monitoring · screen_recording | yes | The scope accepted; one row per (seat, notice version, scope). |
acks[].text_sha256 | string | yes | The hash of the text the seat saw (copied from the notice version). |
acks[].channel | dashboard_checkin | yes | |
acks[].acked_at | string (date-time) | yes | |
next_cursor | string | null | yes | Cursor of the next page; null on the last one. |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (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"
}
}Acknowledge the current screen-monitoring notice from one's OWN seat
http
POST /v1/screen-monitor/acksRecords that the caller's OWN seat accepts notice_version, which must be the CURRENT version, FOR ONE scope: live_monitoring or screen_recording. The scopes are acknowledged separately: accepting live monitoring does not authorize recording, and a new notice version invalidates both. Idempotent: a second acknowledgement of the same version and scope returns the first (200, no new row). The ledger is append-only (DELETE revoked and a trigger refuses UPDATE/DELETE).
The seat is derived from the bearer, never from the body. Only a ddu_ login with a linked seat may acknowledge: a dd_ machine key has no person behind it (403 forbidden), a ddu_ with no active seat is 403 seat_not_linked, and an agent_id in the body that is not the caller's own seat is 403 forbidden — a third party never acknowledges on someone else's behalf. A ddw_ widget token is 403 before the handler.
404 not_found while the tenant's flag screen_monitor_enabled is off. With the flag on, 409 no_notice_published when no version exists and 409 stale_notice when notice_version is not the current one.
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
notice_version | integer | yes | The version being accepted; it must be the current one. |
scope | live_monitoring · screen_recording | yes | What the seat accepts with this acknowledgement. |
agent_id | string (uuid) | no | Optional. If present it must be the caller's own seat; anything else is 403. |
Responses
200
The acknowledgement (the existing one when repeated).
| Field | Type | Required | Description |
|---|---|---|---|
ack | object (ScreenMonitorAck) | yes | One acknowledgement of one notice version by one seat. Append-only. |
ack.id | string (uuid) | yes | |
ack.agent_id | string (uuid) | yes | |
ack.notice_version | integer | yes | |
ack.scope | live_monitoring · screen_recording | yes | The scope accepted; one row per (seat, notice version, scope). |
ack.text_sha256 | string | yes | The hash of the text the seat saw (copied from the notice version). |
ack.channel | dashboard_checkin | yes | |
ack.acked_at | string (date-time) | yes |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409
no_notice_published: no notice version exists yet, so nobody can acknowledge. stale_notice: notice_version is not the current one.
json
{
"error": {
"code": "stale_notice",
"message": "that notice version is not the current one"
}
}The current recording-and-supervision notice an agent accepts
http
GET /v1/agent-monitoring-noticeThe texts (one per language) of the CURRENT version of the notice an agent accepts at check-in (core#2323, MT-CLI-37; ADR #289). Where both parties must consent, the acceptance by agent and date is the proof the law asks for. The notice is a global catalogue (no tenant) and append-only: publishing a newer version invalidates, without touching them, every acceptance of the older ones. Core publishes NO legal text of its own: the catalogue is born empty and is filled by whoever migrates, with the reference of the text's approval.
404 not_found while no version is in force. Reachable by a dd_ key and by a ddu_ login; a ddw_ widget token is 403 before the handler.
Responses
200
The current version and its texts.
| Field | Type | Required | Description |
|---|---|---|---|
notice | object (AgentMonitoringNotice) | yes | The CURRENT version of the recording-and-supervision notice, one text per language (GET /v1/agent-monitoring-notice). Global, append-only. |
notice.version | integer | yes | |
notice.effective_at | string (date-time) | yes | |
notice.texts | array of object | yes | |
notice.texts[].lang | string | yes | Language of the text, e.g. es. |
notice.texts[].title | string | yes | |
notice.texts[].body | string | yes | |
notice.texts[].text_sha256 | string | yes | SHA-256 (hex) of body in UTF-8; an acceptance copies it. |
notice.texts[].published_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"
}
}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"
}
}Accept the current recording-and-supervision notice from one's OWN seat
http
POST /v1/agent-monitoring-acknowledgementsRecords that the caller's OWN seat accepts notice_version, which must be the CURRENT version, as read in lang (a language the current version has a text in). The row keeps the version, the language, the hash of the text the agent saw, the date and the origin (channel). Idempotent: a second acceptance of the same version returns the first (200, no new row), even in another language. The ledger is append-only: it can neither be edited nor deleted (DELETE revoked and a trigger refuses UPDATE/DELETE); a newer notice version leaves the old acceptance on file and simply stops covering the agent.
The seat is derived from the bearer, never from the body. Only a ddu_ login with a linked seat may accept: a dd_ machine key has no person behind it (403 forbidden), a ddu_ with no active seat is 403 seat_not_linked, and an agent_id in the body that is not the caller's own seat is 403 forbidden. A ddw_ widget token is 403 before the handler.
409 no_notice_published when no version is in force and 409 stale_notice when notice_version is not the current one; 422 when the current version has no text in lang.
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
notice_version | integer | yes | The version being accepted; it must be the current one. |
lang | string | yes | The language of the text the agent was shown, e.g. es or en. |
agent_id | string (uuid) | no | Optional. If present it must be the caller's own seat; anything else is 403. |
Responses
200
The acceptance (the existing one when repeated).
| Field | Type | Required | Description |
|---|---|---|---|
acknowledgement | object (AgentMonitoringAck) | yes | One acceptance of one notice version by one agent. Append-only. |
acknowledgement.id | string (uuid) | yes | |
acknowledgement.agent_id | string (uuid) | yes | |
acknowledgement.notice_version | integer | yes | |
acknowledgement.lang | string | yes | The language of the text the agent saw. |
acknowledgement.text_sha256 | string | yes | The hash of the text the agent saw (copied from the notice version). |
acknowledgement.channel | dashboard_checkin | yes | The origin of the acceptance. |
acknowledgement.acked_at | string (date-time) | yes |
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
no_notice_published: no notice version is in force, so nobody can accept. stale_notice: notice_version is not the current one.
json
{
"error": {
"code": "stale_notice",
"message": "that notice version is not the current one"
}
}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)"
}
}Arm or disarm the screen capture held by one browser tab of one's OWN seat
http
PUT /v1/screen-monitor/armedThe agent's dashboard calls this once it holds a retained full-screen capture (nothing is sent anywhere until a supervisor opens a session) and again, with armed: false, when it releases it. Only an armed seat can be watched. Arming needs the seat to be checked in (409 not_checked_in) and to hold an acknowledgement of the CURRENT notice for live_monitoring (409 no_ack).
One tab per seat: arming with a different tab_id DISPLACES the previous one. The previous tab is told with a screen.disarmed frame (reason: displaced) on the seat's private topic, and a live session fed by it is closed as ended / agent_stopped. Arming again with the SAME tab_id changes nothing. Disarming with a tab_id that does not hold the arming is a no-op; the response says whether the seat is still armed.
The armed state lives in the process, not in the ledger: a restart disarms everyone (fail-closed). A checkout also disarms the seat (screen.disarmed, reason: checkout), and so does losing its last socket.
Only a ddu_ login with a linked seat may call it (dd_ key: 403; a ddw_ widget token is 403 before the handler). 404 not_found while the tenant flag screen_monitor_enabled is off.
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
armed | boolean | yes | |
tab_id | string | yes | An opaque id the dashboard gives to the tab holding the capture. |
Responses
200
The state of the seat after the call.
| Field | Type | Required | Description |
|---|---|---|---|
armed | boolean | yes | |
tab_id | string | no | Present when arming; the tab now holding the arming. |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409
not_checked_in: the seat is not on shift. no_ack: no acknowledgement of the current notice for live_monitoring.
json
{
"error": {
"code": "no_ack",
"message": "acknowledge the current screen-monitor notice before arming"
}
}List the screen-monitor session ledger (supervisor or admin seat)
http
GET /v1/screen-monitor/sessionsThe tenant's monitoring sessions, newest first, paginated by cursor: the opened ones, the refused ones and the ones the system closed. Allowed only to a seat whose template role is supervisor or admin (403 role_forbidden otherwise; a dd_ key and a ddu_ with no seat are 403). The role is evaluated always; it does not depend on supervision_role_enforced.
It does NOT consult screen_monitor_enabled: it is the evidence of who watched whom, and turning the function off neither deletes nor hides it. It never carries signalling payload, and nothing crosses tenants.
A started row that is still live closes itself: when the agent or the supervisor loses the socket, checks out, or the flag, the role or the acknowledgement stop holding, the row becomes ended with the reason (flag_off, role, no_ack, checkout, agent_stopped, credential_lapsed or peer_failed).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
limit | query | integer | no | Page size (a value over the ceiling is capped, not refused). |
cursor | query | string | no | Opaque keyset cursor — the next_cursor of the previous page. |
agent_id | query | string (uuid) | no | Only the sessions over this seat. |
result | query | started · ended · failed · refused | no | Only the rows with this result. |
Responses
200
One page of sessions, newest first.
| Field | Type | Required | Description |
|---|---|---|---|
sessions | array of ScreenMonitorSession | yes | |
sessions[].id | string (uuid) | yes | |
sessions[].supervisor_user_id | string (uuid) | yes | The login that asked. |
sessions[].supervisor_id | string (uuid) | null | yes | The asker's seat; null when the login has no seat. |
sessions[].agent_id | string (uuid) | null | yes | The watched seat; null when it did not resolve for the tenant or the role was refused first. |
sessions[].notice_version | integer | null | yes | The notice version in force when the session opened, when the agent had one. |
sessions[].result | started · ended · failed · refused | yes | |
sessions[].reason | flag_off · role · agent_not_found · no_ack · not_armed · agent_busy · agent_stopped · credential_lapsed · checkout · peer_failed | null | yes | Why a session was refused, failed or ended by the system; null for a normal close or while started. |
sessions[].started_at | string (date-time) | yes | |
sessions[].ended_at | string (date-time) | null | yes | |
next_cursor | string | null | yes | Cursor of the next page; null on the last one. |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (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"
}
}Ask to watch an agent's screen (supervisor or admin seat)
http
POST /v1/screen-monitor/sessionsOpens a monitoring session over one agent. Core does not see the video: it validates, writes the ledger row and tells the agent (screen.view_requested on the agent's private topic); the browsers then negotiate the media peer to peer through POST /v1/screen-monitor/sessions/{id}/signal.
EVIDENCE-FIRST: every outcome writes a row of screen_monitor_sessions, also the refused ones. The gates run in this order and the first one that fails is the row's reason: role (the caller's seat must be supervisor or admin; evaluated ALWAYS, it does not depend on supervision_role_enforced; a login with no seat is also role), then agent_not_found, flag_off, no_ack (the agent has no acknowledgement of the current notice for live_monitoring), not_armed and agent_busy (the agent already has a live session). An id of another tenant, an id that does not exist and an agent that is not active are 404 with the SAME body and leave the same row (agent_not_found, agent_id null): nothing here tells whether an agent exists in another tenant. flag_off is the same 404 as every other operation of the function.
If the agent has no live socket to be told, the session is written as failed / peer_failed and the answer is 409 agent_unreachable.
Only a ddu_ login may call it; a dd_ key has no person behind it (403 forbidden, no row). A ddw_ is 403 before the handler.
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
agent_id | string (uuid) | yes | The seat to watch. |
Responses
201
The session was opened; the row is started.
| Field | Type | Required | Description |
|---|---|---|---|
session | object (ScreenMonitorSession) | yes | One row of the screen-monitor session ledger (core#1990). Append-only; it never carries signalling payload. |
session.id | string (uuid) | yes | |
session.supervisor_user_id | string (uuid) | yes | The login that asked. |
session.supervisor_id | string (uuid) | null | yes | The asker's seat; null when the login has no seat. |
session.agent_id | string (uuid) | null | yes | The watched seat; null when it did not resolve for the tenant or the role was refused first. |
session.notice_version | integer | null | yes | The notice version in force when the session opened, when the agent had one. |
session.result | started · ended · failed · refused | yes | |
session.reason | flag_off · role · agent_not_found · no_ack · not_armed · agent_busy · agent_stopped · credential_lapsed · checkout · peer_failed | null | yes | Why a session was refused, failed or ended by the system; null for a normal close or while started. |
session.started_at | string (date-time) | yes | |
session.ended_at | string (date-time) | null | yes |
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
role_forbidden: the caller's seat is not a supervisor or admin (or the login has no seat). forbidden: a dd_ key.
json
{
"error": {
"code": "role_forbidden",
"message": "watching a screen takes a supervisor or admin seat"
}
}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
no_ack, not_armed, agent_busy, or agent_unreachable (see above).
json
{
"error": {
"code": "not_armed",
"message": "the agent is not sharing their screen"
}
}Send one WebRTC signalling message to the other party of a live session
http
POST /v1/screen-monitor/sessions/{id}/signalForwards payload to the OTHER party of the session as a private frame (screen.offer, screen.answer or screen.candidate) and forgets it: the payload carries network addresses and is NEVER stored. The caller must be one of the two parties of a started session; offer can only come from the agent and answer only from the supervisor (403), candidate from either.
A caller who is not a party, an id that does not exist in the tenant and an id that is not a UUID all answer the SAME 404. A closed session is 409 session_closed.
Bounded: the JSON of payload is at most 16384 bytes (413 payload_too_large) and a session accepts at most 60 messages per 10 seconds (429 rate_limited with Retry-After). 202 says the message was handed over; delivered is false when the other party had no live socket.
404 not_found while the tenant flag screen_monitor_enabled is off.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes |
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
kind | offer · answer · candidate | yes | |
payload | object | yes | The SDP or ICE candidate object, passed through untouched. |
Responses
202
Handed over to the other party's private topic.
| Field | Type | Required | Description |
|---|---|---|---|
delivered | boolean | yes |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409
session_closed: the session is no longer started.
json
{
"error": {
"code": "session_closed",
"message": "that session is already closed"
}
}413
payload_too_large: the JSON of payload is over 16384 bytes.
json
{
"error": {
"code": "payload_too_large",
"message": "payload is over 16384 bytes"
}
}429
rate_limited: more than 60 messages in 10 seconds for this session.
json
{
"error": {
"code": "rate_limited",
"message": "too many signalling messages for this session"
}
}Ephemeral TURN credential for ONE monitoring session (either party)
http
GET /v1/screen-monitor/sessions/{id}/iceThe relay a browser needs to carry the screen video when a symmetric NAT blocks the direct path. Only the two parties of a session that is still started get it, with the tenant flag screen_monitor_enabled on. A caller who is not a party, an id that does not exist in the tenant and an id that is not a UUID all answer the SAME 404.
The credential is the cell's own coturn with its use-auth-secret scheme, the same shared secret as the softphone's (no new secret or variable): username is <unix expiry>:scr.<session_id>.<agent|viewer> (one :), credential is Base64(HMAC-SHA1(secret, username)), and it expires one hour after the call (expires_at). The shape is the WebRTC RTCIceServer one, to append to iceServers as is. The response is Cache-Control: no-store; the shared secret is never part of any response or log.
Fail-closed: a cell with no TURN configured answers 503 turn_unavailable, never an ICE list that looks valid and carries no relay.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes |
Responses
200
The relay credential for the caller's side of the session.
| Field | Type | Required | Description |
|---|---|---|---|
ice_server | object | yes | |
ice_server.urls | array of string | yes | turn: / turns: URLs of the cell's relay. |
ice_server.username | string | yes | <unix expiry>:scr.<session_id>.<agent|viewer> |
ice_server.credential | string | yes | Base64 of HMAC-SHA1(shared secret, username). |
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"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409
session_closed: the session is no longer started.
json
{
"error": {
"code": "session_closed",
"message": "that session is already closed"
}
}503
turn_unavailable: this cell has no TURN relay configured.
json
{
"error": {
"code": "turn_unavailable",
"message": "this cell has no TURN relay configured for screen monitoring"
}
}Close a monitoring session (either party)
http
POST /v1/screen-monitor/sessions/{id}/endCloses a started session. Without reason it is a normal close: ended, with reason agent_stopped when the agent asks and no reason when the supervisor does. reason: peer_failed reports that the media connection never came up: the row becomes failed. The other party gets a screen.ended frame on its private topic.
Idempotent: closing a session that is already closed returns the row as it is and writes nothing. A caller who is not a party, an id that does not exist in the tenant and an id that is not a UUID all answer the SAME 404.
It does NOT consult the tenant flag: a session can always be closed.
The system also closes a live session on its own, and tells both parties with screen.ended (result, reason): on the agent's checkout (checkout), when a party's last socket is lost (agent_stopped for the watched seat, peer_failed for the supervisor), when a socket's credential lapses (credential_lapsed), and on a socket's heartbeat when the flag, the supervisor's role or the agent's acknowledgement stopped holding (flag_off, role, no_ack).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
reason | peer_failed | no |
Responses
200
The session after the call.
| Field | Type | Required | Description |
|---|---|---|---|
session | object (ScreenMonitorSession) | yes | One row of the screen-monitor session ledger (core#1990). Append-only; it never carries signalling payload. |
session.id | string (uuid) | yes | |
session.supervisor_user_id | string (uuid) | yes | The login that asked. |
session.supervisor_id | string (uuid) | null | yes | The asker's seat; null when the login has no seat. |
session.agent_id | string (uuid) | null | yes | The watched seat; null when it did not resolve for the tenant or the role was refused first. |
session.notice_version | integer | null | yes | The notice version in force when the session opened, when the agent had one. |
session.result | started · ended · failed · refused | yes | |
session.reason | flag_off · role · agent_not_found · no_ack · not_armed · agent_busy · agent_stopped · credential_lapsed · checkout · peer_failed | null | yes | Why a session was refused, failed or ended by the system; null for a normal close or while started. |
session.started_at | string (date-time) | yes | |
session.ended_at | string (date-time) | null | yes |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}Self-service account settings
http
PATCH /v1/tenantOnly name is tenant-editable. The billing-bearing fields (tier, voice_ai_enabled, seats, spend caps) live on the admin surface (PATCH /v1/admin/tenants/{id}) — letting a tenant edit them with its own bearer is a direct under-pay vector. Other body keys are silently ignored.
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
name | string | no |
json
{
"name": "Acme Collections LLC"
}Responses
200
Updated tenant.
| Field | Type | Required | Description |
|---|---|---|---|
tenant | object (Tenant) | yes | |
tenant.id | string (uuid) | no | |
tenant.name | string | no | |
tenant.account_id | string (uuid) | no | The EMPRESA this tenant (sede) belongs to (DA-2). |
tenant.billing_account_id | string (uuid) | no | The billing account the sede is invoiced under (DA-3). Shared by every sede of a consolidated empresa; one per sede when per_site. |
tenant.status | active · suspended | no | |
tenant.synthetic | boolean | no | true for a demonstration tenant. It originates no traffic: manual dial, SMS send, the external leg of a transfer and the sandbox answer 403 synthetic_tenant. Always present, never null, so a console can warn BEFORE the action fails (core#2512). |
tenant.retention_months | integer | no | |
tenant.seats | integer | no | |
tenant.tier | starter · growth · scale · enterprise · dialer_core · compliance_pro · audit_shield | no | |
tenant.voice_ai_enabled | boolean | no | |
tenant.ai_monthly_budget_usd | number | null | no | Cost-integrity ceiling (admin-set; null = no cap). |
tenant.ai_max_call_seconds | integer | null | no | Per-AI-call hard duration ceiling (seconds) set by the admin. null means the PLATFORM default applies — never "no ceiling" (core#922, ADR #115). |
tenant.ai_max_call_seconds_effective | integer | no | The ceiling FreeSWITCH is actually handed for this tenant's AI legs: ai_max_call_seconds when set, else the cell's platform default (AI_MAX_CALL_SECONDS_DEFAULT, compiled fallback 900). Always present, never null. Read-only (derived). |
tenant.sms_monthly_budget_usd | number | null | no | Month-to-date SMS spend cap. |
tenant.human_initiated_enabled | boolean | no | D17 human-initiated consent posture (admin-set; default false). When true, a LIVE preview/manual/CTI voice dial with no artificial and no AI voice does not require prior express consent (no ATDS). Every other gate — DNC, quiet hours, Reg F, state matrix, artificial voice — still applies unchanged. |
tenant.max_cps | integer | null | no | Per-tenant CPS ceiling (admin-set; null = no ceiling). ADR #12. |
tenant.max_concurrent_channels | integer | null | no | Per-tenant ceiling of concurrent calls, one unit per call with or without an agent (admin-set; null = no ceiling). ADR #12, ADR #185. |
tenant.abandon_seller_name | string | null | no | Seller name for the FTC TSR abandoned-call identification message (admin-set; null = not configured, predictive over-dial stays dark). ADR #14. |
tenant.abandon_seller_phone | string | null | no | E.164 callback number for the abandoned-call identification message (admin-set; null = not configured). ADR #14. |
tenant.tax_country | string | null | no | ISO-3166-1 alpha-2 country of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77. |
tenant.tax_region | string | null | no | State/province code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77. |
tenant.tax_postal_code | string | null | no | Postal code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77. |
tenant.kyc_verified_at | string (date-time) | null | no | When the platform verified this customer (admin-set; null = not yet). A 1-seat account that is not verified cannot create or start a campaign that runs without an agent (403 verification_required, ADR #279). |
tenant.created_at | string (date-time) | no | |
tenant.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"
}
}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"
}
}The tenant's own calling hours
http
GET /v1/tenant/calling-windowThe tenant-wide calling window, or null when the tenant has none (the dial path is then what it was before this key existed). It only NARROWS: a call leaves when the legal window (R18, per destination), the campaign's own window AND this one are all open. Readable by a dd_ key and by any dashboard login.
Responses
200
The stored window, or null.
| Field | Type | Required | Description |
|---|---|---|---|
calling_window | TenantCallingWindow | 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"
}
}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"
}
}503
The stored window could not be read (never answered as "no window").
Set the tenant's calling hours
http
PUT /v1/tenant/calling-windowReplaces the tenant's window. Tenant management only: a dd_ key or an account_admin login; a member login is 403 role_forbidden. The body is validated before anything is stored: a malformed window is 422 with the reason and the previous window stays. The stored value is the normalised one (HH:MM becomes HH:MM:SS). The write is audited.
A stored value that can no longer be evaluated REFUSES the dial (rule_id tenant.calling_window_unavailable.v1): only the absence of a window opens.
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
timezone | string | yes | An IANA zone name. A zone that does not resolve is 422. |
weekly | object (CampaignWeeklyWindow) | yes | Per-weekday windows keyed mon…sun; at least one day. A day that is not a key has no window and is closed. |
weekly.mon | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
weekly.mon.start | string | yes | |
weekly.mon.end | string | yes | |
weekly.tue | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
weekly.tue.start | string | yes | |
weekly.tue.end | string | yes | |
weekly.wed | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
weekly.wed.start | string | yes | |
weekly.wed.end | string | yes | |
weekly.thu | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
weekly.thu.start | string | yes | |
weekly.thu.end | string | yes | |
weekly.fri | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
weekly.fri.start | string | yes | |
weekly.fri.end | string | yes | |
weekly.sat | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
weekly.sat.start | string | yes | |
weekly.sat.end | string | yes | |
weekly.sun | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
weekly.sun.start | string | yes | |
weekly.sun.end | string | yes |
json
{
"timezone": "America/Chicago",
"weekly": {
"mon": {
"start": "09:00",
"end": "20:00"
},
"tue": {
"start": "09:00",
"end": "20:00"
}
}
}Responses
200
Stored (normalised).
| Field | Type | Required | Description |
|---|---|---|---|
calling_window | object (TenantCallingWindow) | yes | The tenant's own calling hours (core#1527, R19 key tenant_calling_window). It only NARROWS: R18 (per destination) and the campaign's window still apply. The hours are read in timezone (the tenant's, NOT the destination's). |
calling_window.timezone | string | yes | An IANA zone name. A zone that does not resolve is 422. |
calling_window.weekly | object (CampaignWeeklyWindow) | yes | Per-weekday windows keyed mon…sun; at least one day. A day that is not a key has no window and is closed. |
calling_window.weekly.mon | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
calling_window.weekly.mon.start | string | yes | |
calling_window.weekly.mon.end | string | yes | |
calling_window.weekly.tue | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
calling_window.weekly.tue.start | string | yes | |
calling_window.weekly.tue.end | string | yes | |
calling_window.weekly.wed | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
calling_window.weekly.wed.start | string | yes | |
calling_window.weekly.wed.end | string | yes | |
calling_window.weekly.thu | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
calling_window.weekly.thu.start | string | yes | |
calling_window.weekly.thu.end | string | yes | |
calling_window.weekly.fri | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
calling_window.weekly.fri.start | string | yes | |
calling_window.weekly.fri.end | string | yes | |
calling_window.weekly.sat | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
calling_window.weekly.sat.start | string | yes | |
calling_window.weekly.sat.end | string | yes | |
calling_window.weekly.sun | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
calling_window.weekly.sun.start | string | yes | |
calling_window.weekly.sun.end | string | 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"
}
}403
role_forbidden: a dashboard login without the account_admin role; forbidden: a widget token or a suspended tenant.
json
{
"error": {
"code": "role_forbidden",
"message": "a dashboard login needs the account_admin role to change the tenant calling window"
}
}422
Malformed window; the message names the reason. Nothing is stored.
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"
}
}Clear the tenant's calling hours
http
DELETE /v1/tenant/calling-windowBack to no tenant window (idempotent). Same roles as PUT. The campaign and R18 windows are untouched.
Responses
200
Cleared.
| Field | Type | Required | Description |
|---|---|---|---|
calling_window | 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"
}
}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"
}
}The tenant's login policy
http
GET /v1/tenant/login-policyThe standing login policy (ADR 227, core#1760), or null when none was ever written (the sign-in is then what it was before the policy existed). It is the allowlist of client addresses and the hours per role that the dashboard sign-in (ddu_) checks AFTER the credential is proven. client_address_measured says whether this cell measures the client address: when it is false an ip_allow cannot be armed here (the PUT answers 409 client_address_not_measured) and only windows can. Readable by a dd_ key and by an account_admin login; a member login is 403 role_forbidden.
Responses
200
The standing version, or null.
| Field | Type | Required | Description |
|---|---|---|---|
login_policy | TenantLoginPolicyVersion | null | yes | |
client_address_measured | boolean | 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"
}
}403
role_forbidden: a dashboard login without the account_admin role; forbidden: a widget token or a suspended 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"
}
}503
The policy could not be read (never answered as "no policy").
Publish a new version of the tenant's login policy
http
PUT /v1/tenant/login-policyAppends a version (nothing is edited in place; see .../history). Only a ddu_ session of an account_admin writes: a dd_ key is 403 role_forbidden, because a stolen key that switches the control off would make it decor.
mode defaults to observe (the sign-in is never denied; each attempt that would have been is recorded so the admin sees who would be left out BEFORE arming enforce). Refused, and nothing stored:
422 unprocessable: malformed body, a CIDR that does not parse, a/0, more than 64 entries, an unknown role, a window that does not parse or a missingtimezone, orenforcewith neitherip_allownorwindows.409 client_address_not_measured: anip_allowinobserveorenforcein a cell whose client address is not measured.422 would_lock_out_caller: with anip_allow, the writer's own address cannot be attributed; withenforce, the writer's own address, role and the time of the request must pass the policy being written.
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
mode | off · observe · enforce | no | off is a version too (switching off is writing, not deleting). observe never denies. enforce denies with 403 login_policy_denied. |
ip_allow | array of string | no | CIDR blocks or bare addresses, IPv4 and IPv6; the families never cross. Empty = no restriction by address. A malformed entry or a /0 is 422. |
windows | object | no | Sign-in hours per role, in the form of a campaign's weekly window (a day without an entry is closed, a window never crosses midnight). A role without an entry has no hours restriction. |
windows.member | object (CampaignWeeklyWindow) | no | Per-weekday windows keyed mon…sun; at least one day. A day that is not a key has no window and is closed. |
windows.member.mon | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
windows.member.mon.start | string | yes | |
windows.member.mon.end | string | yes | |
windows.member.tue | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
windows.member.tue.start | string | yes | |
windows.member.tue.end | string | yes | |
windows.member.wed | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
windows.member.wed.start | string | yes | |
windows.member.wed.end | string | yes | |
windows.member.thu | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
windows.member.thu.start | string | yes | |
windows.member.thu.end | string | yes | |
windows.member.fri | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
windows.member.fri.start | string | yes | |
windows.member.fri.end | string | yes | |
windows.member.sat | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
windows.member.sat.start | string | yes | |
windows.member.sat.end | string | yes | |
windows.member.sun | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
windows.member.sun.start | string | yes | |
windows.member.sun.end | string | yes | |
windows.account_admin | object (CampaignWeeklyWindow) | no | Per-weekday windows keyed mon…sun; at least one day. A day that is not a key has no window and is closed. |
windows.account_admin.mon | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
windows.account_admin.mon.start | string | yes | |
windows.account_admin.mon.end | string | yes | |
windows.account_admin.tue | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
windows.account_admin.tue.start | string | yes | |
windows.account_admin.tue.end | string | yes | |
windows.account_admin.wed | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
windows.account_admin.wed.start | string | yes | |
windows.account_admin.wed.end | string | yes | |
windows.account_admin.thu | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
windows.account_admin.thu.start | string | yes | |
windows.account_admin.thu.end | string | yes | |
windows.account_admin.fri | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
windows.account_admin.fri.start | string | yes | |
windows.account_admin.fri.end | string | yes | |
windows.account_admin.sat | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
windows.account_admin.sat.start | string | yes | |
windows.account_admin.sat.end | string | yes | |
windows.account_admin.sun | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
windows.account_admin.sun.start | string | yes | |
windows.account_admin.sun.end | string | yes | |
timezone | string,null | no | An IANA zone; required when windows is not empty, refused otherwise. |
reason | string,null | no |
json
{
"mode": "observe",
"ip_allow": [
"203.0.113.0/24"
],
"windows": {
"member": {
"mon": {
"start": "08:00",
"end": "18:00"
},
"tue": {
"start": "08:00",
"end": "18:00"
}
}
},
"timezone": "America/Chicago",
"reason": "Office hours only"
}Responses
200
The version that was stored.
| Field | Type | Required | Description |
|---|---|---|---|
login_policy | object (TenantLoginPolicyVersion) | yes | One published (append-only) version of the tenant's login policy. |
login_policy.version | integer | yes | |
login_policy.mode | off · observe · enforce | yes | |
login_policy.ip_allow | array of string | yes | |
login_policy.windows | object | yes | |
login_policy.windows.member | object (CampaignWeeklyWindow) | no | Per-weekday windows keyed mon…sun; at least one day. A day that is not a key has no window and is closed. |
login_policy.windows.member.mon | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
login_policy.windows.member.tue | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
login_policy.windows.member.wed | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
login_policy.windows.member.thu | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
login_policy.windows.member.fri | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
login_policy.windows.member.sat | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
login_policy.windows.member.sun | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
login_policy.windows.account_admin | object (CampaignWeeklyWindow) | no | Per-weekday windows keyed mon…sun; at least one day. A day that is not a key has no window and is closed. |
login_policy.windows.account_admin.mon | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
login_policy.windows.account_admin.tue | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
login_policy.windows.account_admin.wed | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
login_policy.windows.account_admin.thu | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
login_policy.windows.account_admin.fri | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
login_policy.windows.account_admin.sat | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
login_policy.windows.account_admin.sun | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
login_policy.timezone | string,null | yes | |
login_policy.set_by | string | yes | |
login_policy.reason | string,null | yes | |
login_policy.inserted_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"
}
}403
role_forbidden: a dd_ key or a dashboard login without the account_admin role; forbidden: a widget token or a suspended tenant.
json
{
"error": {
"code": "role_forbidden",
"message": "writing the login policy takes a dashboard login with the account_admin role"
}
}409
client_address_not_measured: this cell does not measure the client address, so an ip_allow would compare against the proxy. Nothing is stored.
422
unprocessable (the message names the reason) or would_lock_out_caller (this policy would shut the writer out). Nothing is stored.
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"
}
}Every published version of the tenant's login policy
http
GET /v1/tenant/login-policy/historyNewest first. set_by is user:<id> for a version the tenant wrote and operator:<id> for a recovery by platform staff. Same readers as GET /v1/tenant/login-policy.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
limit | query | integer | no | Page size (a value over the ceiling is capped, not refused). |
cursor | query | integer | no | The next_cursor of the previous page. |
Responses
200
A page of versions.
| Field | Type | Required | Description |
|---|---|---|---|
versions | array of TenantLoginPolicyVersion | yes | |
versions[].version | integer | yes | |
versions[].mode | off · observe · enforce | yes | |
versions[].ip_allow | array of string | yes | |
versions[].windows | object | yes | |
versions[].windows.member | object (CampaignWeeklyWindow) | no | Per-weekday windows keyed mon…sun; at least one day. A day that is not a key has no window and is closed. |
versions[].windows.member.mon | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
versions[].windows.member.tue | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
versions[].windows.member.wed | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
versions[].windows.member.thu | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
versions[].windows.member.fri | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
versions[].windows.member.sat | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
versions[].windows.member.sun | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
versions[].windows.account_admin | object (CampaignWeeklyWindow) | no | Per-weekday windows keyed mon…sun; at least one day. A day that is not a key has no window and is closed. |
versions[].windows.account_admin.mon | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
versions[].windows.account_admin.tue | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
versions[].windows.account_admin.wed | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
versions[].windows.account_admin.thu | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
versions[].windows.account_admin.fri | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
versions[].windows.account_admin.sat | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
versions[].windows.account_admin.sun | object (CampaignDayWindow) | no | One weekday's window, within the day: start strictly before end. HH:MM is accepted and stored as HH:MM:SS. No offset: the window is read in the zone the campaign reads the hour in. |
versions[].timezone | string,null | yes | |
versions[].set_by | string | yes | |
versions[].reason | string,null | yes | |
versions[].inserted_at | string (date-time) | yes | |
next_cursor | integer,null | yes | Pass as cursor for the next page; null on the last. |
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
role_forbidden: a dashboard login without the account_admin role; forbidden: a widget token or a suspended 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"
}
}503
The history could not be read.
List tenants (always one element)
http
GET /v1/tenantsDocs parity: API keys are tenant-scoped, so the list contains exactly the authenticated tenant.
Responses
200
One-element list (own tenant).
| Field | Type | Required | Description |
|---|---|---|---|
tenants | array of Tenant | yes | |
tenants[].id | string (uuid) | no | |
tenants[].name | string | no | |
tenants[].account_id | string (uuid) | no | The EMPRESA this tenant (sede) belongs to (DA-2). |
tenants[].billing_account_id | string (uuid) | no | The billing account the sede is invoiced under (DA-3). Shared by every sede of a consolidated empresa; one per sede when per_site. |
tenants[].status | active · suspended | no | |
tenants[].synthetic | boolean | no | true for a demonstration tenant. It originates no traffic: manual dial, SMS send, the external leg of a transfer and the sandbox answer 403 synthetic_tenant. Always present, never null, so a console can warn BEFORE the action fails (core#2512). |
tenants[].retention_months | integer | no | |
tenants[].seats | integer | no | |
tenants[].tier | starter · growth · scale · enterprise · dialer_core · compliance_pro · audit_shield | no | |
tenants[].voice_ai_enabled | boolean | no | |
tenants[].ai_monthly_budget_usd | number | null | no | Cost-integrity ceiling (admin-set; null = no cap). |
tenants[].ai_max_call_seconds | integer | null | no | Per-AI-call hard duration ceiling (seconds) set by the admin. null means the PLATFORM default applies — never "no ceiling" (core#922, ADR #115). |
tenants[].ai_max_call_seconds_effective | integer | no | The ceiling FreeSWITCH is actually handed for this tenant's AI legs: ai_max_call_seconds when set, else the cell's platform default (AI_MAX_CALL_SECONDS_DEFAULT, compiled fallback 900). Always present, never null. Read-only (derived). |
tenants[].sms_monthly_budget_usd | number | null | no | Month-to-date SMS spend cap. |
tenants[].human_initiated_enabled | boolean | no | D17 human-initiated consent posture (admin-set; default false). When true, a LIVE preview/manual/CTI voice dial with no artificial and no AI voice does not require prior express consent (no ATDS). Every other gate — DNC, quiet hours, Reg F, state matrix, artificial voice — still applies unchanged. |
tenants[].max_cps | integer | null | no | Per-tenant CPS ceiling (admin-set; null = no ceiling). ADR #12. |
tenants[].max_concurrent_channels | integer | null | no | Per-tenant ceiling of concurrent calls, one unit per call with or without an agent (admin-set; null = no ceiling). ADR #12, ADR #185. |
tenants[].abandon_seller_name | string | null | no | Seller name for the FTC TSR abandoned-call identification message (admin-set; null = not configured, predictive over-dial stays dark). ADR #14. |
tenants[].abandon_seller_phone | string | null | no | E.164 callback number for the abandoned-call identification message (admin-set; null = not configured). ADR #14. |
tenants[].tax_country | string | null | no | ISO-3166-1 alpha-2 country of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77. |
tenants[].tax_region | string | null | no | State/province code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77. |
tenants[].tax_postal_code | string | null | no | Postal code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77. |
tenants[].kyc_verified_at | string (date-time) | null | no | When the platform verified this customer (admin-set; null = not yet). A 1-seat account that is not verified cannot create or start a campaign that runs without an agent (403 verification_required, ADR #279). |
tenants[].created_at | string (date-time) | no | |
tenants[].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 (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"
}
}Fetch own tenant by id
http
GET /v1/tenants/{id}Own tenant id → the tenant. ANY other id → 404 (never a 403; no existence leak).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The tenant.
| Field | Type | Required | Description |
|---|---|---|---|
tenant | object (Tenant) | yes | |
tenant.id | string (uuid) | no | |
tenant.name | string | no | |
tenant.account_id | string (uuid) | no | The EMPRESA this tenant (sede) belongs to (DA-2). |
tenant.billing_account_id | string (uuid) | no | The billing account the sede is invoiced under (DA-3). Shared by every sede of a consolidated empresa; one per sede when per_site. |
tenant.status | active · suspended | no | |
tenant.synthetic | boolean | no | true for a demonstration tenant. It originates no traffic: manual dial, SMS send, the external leg of a transfer and the sandbox answer 403 synthetic_tenant. Always present, never null, so a console can warn BEFORE the action fails (core#2512). |
tenant.retention_months | integer | no | |
tenant.seats | integer | no | |
tenant.tier | starter · growth · scale · enterprise · dialer_core · compliance_pro · audit_shield | no | |
tenant.voice_ai_enabled | boolean | no | |
tenant.ai_monthly_budget_usd | number | null | no | Cost-integrity ceiling (admin-set; null = no cap). |
tenant.ai_max_call_seconds | integer | null | no | Per-AI-call hard duration ceiling (seconds) set by the admin. null means the PLATFORM default applies — never "no ceiling" (core#922, ADR #115). |
tenant.ai_max_call_seconds_effective | integer | no | The ceiling FreeSWITCH is actually handed for this tenant's AI legs: ai_max_call_seconds when set, else the cell's platform default (AI_MAX_CALL_SECONDS_DEFAULT, compiled fallback 900). Always present, never null. Read-only (derived). |
tenant.sms_monthly_budget_usd | number | null | no | Month-to-date SMS spend cap. |
tenant.human_initiated_enabled | boolean | no | D17 human-initiated consent posture (admin-set; default false). When true, a LIVE preview/manual/CTI voice dial with no artificial and no AI voice does not require prior express consent (no ATDS). Every other gate — DNC, quiet hours, Reg F, state matrix, artificial voice — still applies unchanged. |
tenant.max_cps | integer | null | no | Per-tenant CPS ceiling (admin-set; null = no ceiling). ADR #12. |
tenant.max_concurrent_channels | integer | null | no | Per-tenant ceiling of concurrent calls, one unit per call with or without an agent (admin-set; null = no ceiling). ADR #12, ADR #185. |
tenant.abandon_seller_name | string | null | no | Seller name for the FTC TSR abandoned-call identification message (admin-set; null = not configured, predictive over-dial stays dark). ADR #14. |
tenant.abandon_seller_phone | string | null | no | E.164 callback number for the abandoned-call identification message (admin-set; null = not configured). ADR #14. |
tenant.tax_country | string | null | no | ISO-3166-1 alpha-2 country of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77. |
tenant.tax_region | string | null | no | State/province code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77. |
tenant.tax_postal_code | string | null | no | Postal code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77. |
tenant.kyc_verified_at | string (date-time) | null | no | When the platform verified this customer (admin-set; null = not yet). A 1-seat account that is not verified cannot create or start a campaign that runs without an agent (403 verification_required, ADR #279). |
tenant.created_at | string (date-time) | no | |
tenant.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 (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"
}
}