Skip to content

Identity ​

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

Español

Authenticated tenant, account settings.

Endpoints ​

MethodPathSummary
GET/v1/meAuthenticated tenant, user and derived SEAT
GET/v1/me/scorecardThe caller's OWN shift scorecard (seat derived from the token)
GET/v1/screen-monitor/noticeThe current screen-monitoring notice
GET/v1/screen-monitor/acksAcknowledgements of the tenant (supervisor/admin seat)
POST/v1/screen-monitor/acksAcknowledge the current screen-monitoring notice from one's OWN seat
GET/v1/agent-monitoring-noticeThe current recording-and-supervision notice an agent accepts
POST/v1/agent-monitoring-acknowledgementsAccept the current recording-and-supervision notice from one's OWN seat
PUT/v1/screen-monitor/armedArm or disarm the screen capture held by one browser tab of one's OWN seat
GET/v1/screen-monitor/sessionsList the screen-monitor session ledger (supervisor or admin seat)
POST/v1/screen-monitor/sessionsAsk to watch an agent's screen (supervisor or admin seat)
POST/v1/screen-monitor/sessions/{id}/signalSend one WebRTC signalling message to the other party of a live session
GET/v1/screen-monitor/sessions/{id}/iceEphemeral TURN credential for ONE monitoring session (either party)
POST/v1/screen-monitor/sessions/{id}/endClose a monitoring session (either party)
PATCH/v1/tenantSelf-service account settings
GET/v1/tenant/calling-windowThe tenant's own calling hours
PUT/v1/tenant/calling-windowSet the tenant's calling hours
DELETE/v1/tenant/calling-windowClear the tenant's calling hours
GET/v1/tenant/login-policyThe tenant's login policy
PUT/v1/tenant/login-policyPublish a new version of the tenant's login policy
GET/v1/tenant/login-policy/historyEvery published version of the tenant's login policy
GET/v1/tenantsList tenants (always one element)
GET/v1/tenants/{id}Fetch own tenant by id

Authenticated tenant, user and derived SEAT ​

http
GET /v1/me

Returns 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 — seat is the caller's own agent row, in the same projection GET /v1/agents/{id} returns.
  • no_identity — a dd_ machine key: no person behind it, so there is no "own" seat. seat is null; the body is NOT consulted.
  • seat_not_linked — a ddu_ person with no active seat (e.g. a supervisor with no dialing position). seat is null.
  • seat_gone — the bearer points at a deleted agent row. Reachable only by the ddw_ 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.

FieldTypeRequiredDescription
tenantobject (Tenant)yes
tenant.idstring (uuid)no
tenant.namestringno
tenant.account_idstring (uuid)noThe EMPRESA this tenant (sede) belongs to (DA-2).
tenant.billing_account_idstring (uuid)noThe billing account the sede is invoiced under (DA-3). Shared by every sede of a consolidated empresa; one per sede when per_site.
tenant.statusactive · suspendedno
tenant.syntheticbooleannotrue 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_monthsintegerno
tenant.seatsintegerno
tenant.tierstarter · growth · scale · enterprise · dialer_core · compliance_pro · audit_shieldno
tenant.voice_ai_enabledbooleanno
tenant.ai_monthly_budget_usdnumber | nullnoCost-integrity ceiling (admin-set; null = no cap).
tenant.ai_max_call_secondsinteger | nullnoPer-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_effectiveintegernoThe 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_usdnumber | nullnoMonth-to-date SMS spend cap.
tenant.human_initiated_enabledbooleannoD17 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_cpsinteger | nullnoPer-tenant CPS ceiling (admin-set; null = no ceiling). ADR #12.
tenant.max_concurrent_channelsinteger | nullnoPer-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_namestring | nullnoSeller name for the FTC TSR abandoned-call identification message (admin-set; null = not configured, predictive over-dial stays dark). ADR #14.
tenant.abandon_seller_phonestring | nullnoE.164 callback number for the abandoned-call identification message (admin-set; null = not configured). ADR #14.
tenant.tax_countrystring | nullnoISO-3166-1 alpha-2 country of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77.
tenant.tax_regionstring | nullnoState/province code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77.
tenant.tax_postal_codestring | nullnoPostal code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77.
tenant.kyc_verified_atstring (date-time) | nullnoWhen 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_atstring (date-time)no
tenant.updated_atstring (date-time)no
userobject (User)noA dashboard user — the password hash is never exposed.
user.idstring (uuid)no
user.emailstring (email)noGlobal login identifier, stored lowercase.
user.namestringno
user.statusactive · disabledno
user.rolemember · account_adminnoIntra-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_atstring (date-time)no
user.last_login_atstring (date-time) | nullno
user.mfa_enabledbooleannoWhether 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_atstring (date-time)no
user.updated_atstring (date-time)no
supportobjectnoOnly for a dds_ support session (ADR #225): the session is read-only and ends at expires_at.
support.read_onlytrueyes
support.session_idstring (uuid)yes
support.expires_atstring (date-time)yes
support.operatorstringyesThe platform operator who opened it, operator:<id> (<name>).
seatAgent | nullyesThe caller's own seat, or null when seat_source is not token.
seat_sourcetoken · no_identity · seat_not_linked · seat_goneyesHow the seat was resolved (or why there is none). Always present.
capabilitiesobjectyesPer-tenant UI capabilities (core#1806), a closed list. A capability that cannot be resolved reads false.
capabilities.conferencebooleanyestrue 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_channelbooleannotrue 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_monitorobject (ScreenMonitorStatus)yesWhat 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.enabledbooleanyes
screen_monitor.notice_ack_requiredbooleanyes
screen_monitor.notice_versioninteger | nullyesThe current notice version, or null when none is published or the function is off.
screen_monitor.scopesobjectyesPer 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_monitoringobject (ScreenMonitorScopeStatus)yes
screen_monitor.scopes.live_monitoring.ack_currentbooleanyes
screen_monitor.scopes.live_monitoring.acked_atstring (date-time) | nullyesWhen the seat acknowledged the current version for this scope, or null.
screen_monitor.scopes.screen_recordingobject (ScreenMonitorScopeStatus)yes
screen_monitor.scopes.screen_recording.ack_currentbooleanyes
screen_monitor.scopes.screen_recording.acked_atstring (date-time) | nullyesWhen 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/scorecard

The 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

NameInTypeRequiredDescription
fromquerystring (date-time)noInclusive start (RFC 3339). Defaults to the seat's active checked_in_at, or to now - 12h when there is none — see window.source.
toquerystring (date-time)noExclusive end (RFC 3339); must be after from. Defaults to now.

Responses

200 ​

The caller's own scorecard for the resolved window.

FieldTypeRequiredDescription
agentobjectyesThe seat DERIVED from the bearer — echoed so the client can show whose marker it is, never to let the client choose it.
agent.idstring (uuid)yes
agent.namestringyes
agent.sip_extensionstring | nullyes
windowobjectyes
window.fromstring (date-time)yes
window.tostring (date-time)yes
window.sourcecheckin · explicit · fallback_12hyesWhere 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.
callsobjectyesCounted 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.totalintegeryes
calls.in_flightintegeryesAttempts with no settling correction yet. Counted apart instead of being given a fabricated disposition.
calls.by_dispositionobjectyesAttempt counts keyed by the disposition of the LAST settling correction written for the attempt.
promisesobjectyesPromises this seat RECORDED in the window (inserted_at, not promised_date: a shift contains promises dated weeks ahead).
promises.countintegeryes
promises.by_statusobjectyes
promises.by_status.pendingintegeryes
promises.by_status.keptintegeryes
promises.by_status.brokenintegeryes
promises.by_status.cancelledintegeryes
promises.amount_centsobjectyesSummed 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.USDintegeryes
promises.amount_cents.MXNintegeryes

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/notice

The 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.

FieldTypeRequiredDescription
noticeobject (ScreenMonitorNotice)yesOne version of the screen-monitoring notice (GET /v1/screen-monitor/notice). Global, append-only.
notice.versionintegeryes
notice.langstringyesLanguage of the text, e.g. es.
notice.titlestringyes
notice.bodystringyes
notice.text_sha256stringyesSHA-256 (hex) of body in UTF-8; an acknowledgement copies it.
notice.effective_atstring (date-time)yes
notice.published_atstring (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/acks

The 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

NameInTypeRequiredDescription
limitqueryintegernoPage size (a value over the ceiling is capped, not refused).
cursorquerystringnoOpaque keyset cursor — the next_cursor of the previous page.
agent_idquerystring (uuid)noOnly the acknowledgements of this seat.

Responses

200 ​

One page of acknowledgements, newest first.

FieldTypeRequiredDescription
acksarray of ScreenMonitorAckyes
acks[].idstring (uuid)yes
acks[].agent_idstring (uuid)yes
acks[].notice_versionintegeryes
acks[].scopelive_monitoring · screen_recordingyesThe scope accepted; one row per (seat, notice version, scope).
acks[].text_sha256stringyesThe hash of the text the seat saw (copied from the notice version).
acks[].channeldashboard_checkinyes
acks[].acked_atstring (date-time)yes
next_cursorstring | nullyesCursor of the next page; null on the last one.

400 — BadRequest ​

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "error": {
    "code": "bad_request",
    "message": "from must be RFC-3339"
  }
}

401 — Unauthorized ​

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

403 — Forbidden ​

The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (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/acks

Records 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)

FieldTypeRequiredDescription
notice_versionintegeryesThe version being accepted; it must be the current one.
scopelive_monitoring · screen_recordingyesWhat the seat accepts with this acknowledgement.
agent_idstring (uuid)noOptional. If present it must be the caller's own seat; anything else is 403.

Responses

200 ​

The acknowledgement (the existing one when repeated).

FieldTypeRequiredDescription
ackobject (ScreenMonitorAck)yesOne acknowledgement of one notice version by one seat. Append-only.
ack.idstring (uuid)yes
ack.agent_idstring (uuid)yes
ack.notice_versionintegeryes
ack.scopelive_monitoring · screen_recordingyesThe scope accepted; one row per (seat, notice version, scope).
ack.text_sha256stringyesThe hash of the text the seat saw (copied from the notice version).
ack.channeldashboard_checkinyes
ack.acked_atstring (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-notice

The 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.

FieldTypeRequiredDescription
noticeobject (AgentMonitoringNotice)yesThe CURRENT version of the recording-and-supervision notice, one text per language (GET /v1/agent-monitoring-notice). Global, append-only.
notice.versionintegeryes
notice.effective_atstring (date-time)yes
notice.textsarray of objectyes
notice.texts[].langstringyesLanguage of the text, e.g. es.
notice.texts[].titlestringyes
notice.texts[].bodystringyes
notice.texts[].text_sha256stringyesSHA-256 (hex) of body in UTF-8; an acceptance copies it.
notice.texts[].published_atstring (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-acknowledgements

Records 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)

FieldTypeRequiredDescription
notice_versionintegeryesThe version being accepted; it must be the current one.
langstringyesThe language of the text the agent was shown, e.g. es or en.
agent_idstring (uuid)noOptional. If present it must be the caller's own seat; anything else is 403.

Responses

200 ​

The acceptance (the existing one when repeated).

FieldTypeRequiredDescription
acknowledgementobject (AgentMonitoringAck)yesOne acceptance of one notice version by one agent. Append-only.
acknowledgement.idstring (uuid)yes
acknowledgement.agent_idstring (uuid)yes
acknowledgement.notice_versionintegeryes
acknowledgement.langstringyesThe language of the text the agent saw.
acknowledgement.text_sha256stringyesThe hash of the text the agent saw (copied from the notice version).
acknowledgement.channeldashboard_checkinyesThe origin of the acceptance.
acknowledgement.acked_atstring (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/armed

The 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)

FieldTypeRequiredDescription
armedbooleanyes
tab_idstringyesAn opaque id the dashboard gives to the tab holding the capture.

Responses

200 ​

The state of the seat after the call.

FieldTypeRequiredDescription
armedbooleanyes
tab_idstringnoPresent 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/sessions

The 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

NameInTypeRequiredDescription
limitqueryintegernoPage size (a value over the ceiling is capped, not refused).
cursorquerystringnoOpaque keyset cursor — the next_cursor of the previous page.
agent_idquerystring (uuid)noOnly the sessions over this seat.
resultquerystarted · ended · failed · refusednoOnly the rows with this result.

Responses

200 ​

One page of sessions, newest first.

FieldTypeRequiredDescription
sessionsarray of ScreenMonitorSessionyes
sessions[].idstring (uuid)yes
sessions[].supervisor_user_idstring (uuid)yesThe login that asked.
sessions[].supervisor_idstring (uuid) | nullyesThe asker's seat; null when the login has no seat.
sessions[].agent_idstring (uuid) | nullyesThe watched seat; null when it did not resolve for the tenant or the role was refused first.
sessions[].notice_versioninteger | nullyesThe notice version in force when the session opened, when the agent had one.
sessions[].resultstarted · ended · failed · refusedyes
sessions[].reasonflag_off · role · agent_not_found · no_ack · not_armed · agent_busy · agent_stopped · credential_lapsed · checkout · peer_failed | nullyesWhy a session was refused, failed or ended by the system; null for a normal close or while started.
sessions[].started_atstring (date-time)yes
sessions[].ended_atstring (date-time) | nullyes
next_cursorstring | nullyesCursor of the next page; null on the last one.

400 — BadRequest ​

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "error": {
    "code": "bad_request",
    "message": "from must be RFC-3339"
  }
}

401 — Unauthorized ​

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

403 — Forbidden ​

The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (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/sessions

Opens 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)

FieldTypeRequiredDescription
agent_idstring (uuid)yesThe seat to watch.

Responses

201 ​

The session was opened; the row is started.

FieldTypeRequiredDescription
sessionobject (ScreenMonitorSession)yesOne row of the screen-monitor session ledger (core#1990). Append-only; it never carries signalling payload.
session.idstring (uuid)yes
session.supervisor_user_idstring (uuid)yesThe login that asked.
session.supervisor_idstring (uuid) | nullyesThe asker's seat; null when the login has no seat.
session.agent_idstring (uuid) | nullyesThe watched seat; null when it did not resolve for the tenant or the role was refused first.
session.notice_versioninteger | nullyesThe notice version in force when the session opened, when the agent had one.
session.resultstarted · ended · failed · refusedyes
session.reasonflag_off · role · agent_not_found · no_ack · not_armed · agent_busy · agent_stopped · credential_lapsed · checkout · peer_failed | nullyesWhy a session was refused, failed or ended by the system; null for a normal close or while started.
session.started_atstring (date-time)yes
session.ended_atstring (date-time) | nullyes

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}/signal

Forwards 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

NameInTypeRequiredDescription
idpathstring (uuid)yes

Request body (JSON, required)

FieldTypeRequiredDescription
kindoffer · answer · candidateyes
payloadobjectyesThe SDP or ICE candidate object, passed through untouched.

Responses

202 ​

Handed over to the other party's private topic.

FieldTypeRequiredDescription
deliveredbooleanyes

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}/ice

The 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

NameInTypeRequiredDescription
idpathstring (uuid)yes

Responses

200 ​

The relay credential for the caller's side of the session.

FieldTypeRequiredDescription
ice_serverobjectyes
ice_server.urlsarray of stringyesturn: / turns: URLs of the cell's relay.
ice_server.usernamestringyes<unix expiry>:scr.<session_id>.<agent|viewer>
ice_server.credentialstringyesBase64 of HMAC-SHA1(shared secret, username).
expires_atstring (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}/end

Closes 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

NameInTypeRequiredDescription
idpathstring (uuid)yes

Request body (JSON)

FieldTypeRequiredDescription
reasonpeer_failedno

Responses

200 ​

The session after the call.

FieldTypeRequiredDescription
sessionobject (ScreenMonitorSession)yesOne row of the screen-monitor session ledger (core#1990). Append-only; it never carries signalling payload.
session.idstring (uuid)yes
session.supervisor_user_idstring (uuid)yesThe login that asked.
session.supervisor_idstring (uuid) | nullyesThe asker's seat; null when the login has no seat.
session.agent_idstring (uuid) | nullyesThe watched seat; null when it did not resolve for the tenant or the role was refused first.
session.notice_versioninteger | nullyesThe notice version in force when the session opened, when the agent had one.
session.resultstarted · ended · failed · refusedyes
session.reasonflag_off · role · agent_not_found · no_ack · not_armed · agent_busy · agent_stopped · credential_lapsed · checkout · peer_failed | nullyesWhy a session was refused, failed or ended by the system; null for a normal close or while started.
session.started_atstring (date-time)yes
session.ended_atstring (date-time) | nullyes

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/tenant

Only 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)

FieldTypeRequiredDescription
namestringno
json
{
  "name": "Acme Collections LLC"
}

Responses

200 ​

Updated tenant.

FieldTypeRequiredDescription
tenantobject (Tenant)yes
tenant.idstring (uuid)no
tenant.namestringno
tenant.account_idstring (uuid)noThe EMPRESA this tenant (sede) belongs to (DA-2).
tenant.billing_account_idstring (uuid)noThe billing account the sede is invoiced under (DA-3). Shared by every sede of a consolidated empresa; one per sede when per_site.
tenant.statusactive · suspendedno
tenant.syntheticbooleannotrue 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_monthsintegerno
tenant.seatsintegerno
tenant.tierstarter · growth · scale · enterprise · dialer_core · compliance_pro · audit_shieldno
tenant.voice_ai_enabledbooleanno
tenant.ai_monthly_budget_usdnumber | nullnoCost-integrity ceiling (admin-set; null = no cap).
tenant.ai_max_call_secondsinteger | nullnoPer-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_effectiveintegernoThe 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_usdnumber | nullnoMonth-to-date SMS spend cap.
tenant.human_initiated_enabledbooleannoD17 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_cpsinteger | nullnoPer-tenant CPS ceiling (admin-set; null = no ceiling). ADR #12.
tenant.max_concurrent_channelsinteger | nullnoPer-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_namestring | nullnoSeller name for the FTC TSR abandoned-call identification message (admin-set; null = not configured, predictive over-dial stays dark). ADR #14.
tenant.abandon_seller_phonestring | nullnoE.164 callback number for the abandoned-call identification message (admin-set; null = not configured). ADR #14.
tenant.tax_countrystring | nullnoISO-3166-1 alpha-2 country of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77.
tenant.tax_regionstring | nullnoState/province code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77.
tenant.tax_postal_codestring | nullnoPostal code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77.
tenant.kyc_verified_atstring (date-time) | nullnoWhen 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_atstring (date-time)no
tenant.updated_atstring (date-time)no

400 — BadRequest ​

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "error": {
    "code": "bad_request",
    "message": "from must be RFC-3339"
  }
}

401 — Unauthorized ​

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

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-window

The 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.

FieldTypeRequiredDescription
calling_windowTenantCallingWindow | nullyes

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-window

Replaces 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)

FieldTypeRequiredDescription
timezonestringyesAn IANA zone name. A zone that does not resolve is 422.
weeklyobject (CampaignWeeklyWindow)yesPer-weekday windows keyed mon…sun; at least one day. A day that is not a key has no window and is closed.
weekly.monobject (CampaignDayWindow)noOne 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.startstringyes
weekly.mon.endstringyes
weekly.tueobject (CampaignDayWindow)noOne 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.startstringyes
weekly.tue.endstringyes
weekly.wedobject (CampaignDayWindow)noOne 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.startstringyes
weekly.wed.endstringyes
weekly.thuobject (CampaignDayWindow)noOne 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.startstringyes
weekly.thu.endstringyes
weekly.friobject (CampaignDayWindow)noOne 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.startstringyes
weekly.fri.endstringyes
weekly.satobject (CampaignDayWindow)noOne 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.startstringyes
weekly.sat.endstringyes
weekly.sunobject (CampaignDayWindow)noOne 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.startstringyes
weekly.sun.endstringyes
json
{
  "timezone": "America/Chicago",
  "weekly": {
    "mon": {
      "start": "09:00",
      "end": "20:00"
    },
    "tue": {
      "start": "09:00",
      "end": "20:00"
    }
  }
}

Responses

200 ​

Stored (normalised).

FieldTypeRequiredDescription
calling_windowobject (TenantCallingWindow)yesThe 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.timezonestringyesAn IANA zone name. A zone that does not resolve is 422.
calling_window.weeklyobject (CampaignWeeklyWindow)yesPer-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.monobject (CampaignDayWindow)noOne 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.startstringyes
calling_window.weekly.mon.endstringyes
calling_window.weekly.tueobject (CampaignDayWindow)noOne 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.startstringyes
calling_window.weekly.tue.endstringyes
calling_window.weekly.wedobject (CampaignDayWindow)noOne 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.startstringyes
calling_window.weekly.wed.endstringyes
calling_window.weekly.thuobject (CampaignDayWindow)noOne 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.startstringyes
calling_window.weekly.thu.endstringyes
calling_window.weekly.friobject (CampaignDayWindow)noOne 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.startstringyes
calling_window.weekly.fri.endstringyes
calling_window.weekly.satobject (CampaignDayWindow)noOne 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.startstringyes
calling_window.weekly.sat.endstringyes
calling_window.weekly.sunobject (CampaignDayWindow)noOne 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.startstringyes
calling_window.weekly.sun.endstringyes

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-window

Back to no tenant window (idempotent). Same roles as PUT. The campaign and R18 windows are untouched.

Responses

200 ​

Cleared.

FieldTypeRequiredDescription
calling_windownullyes

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-policy

The 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.

FieldTypeRequiredDescription
login_policyTenantLoginPolicyVersion | nullyes
client_address_measuredbooleanyes

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-policy

Appends 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 missing timezone, or enforce with neither ip_allow nor windows.
  • 409 client_address_not_measured: an ip_allow in observe or enforce in a cell whose client address is not measured.
  • 422 would_lock_out_caller: with an ip_allow, the writer's own address cannot be attributed; with enforce, the writer's own address, role and the time of the request must pass the policy being written.

Request body (JSON, required)

FieldTypeRequiredDescription
modeoff · observe · enforcenooff is a version too (switching off is writing, not deleting). observe never denies. enforce denies with 403 login_policy_denied.
ip_allowarray of stringnoCIDR blocks or bare addresses, IPv4 and IPv6; the families never cross. Empty = no restriction by address. A malformed entry or a /0 is 422.
windowsobjectnoSign-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.memberobject (CampaignWeeklyWindow)noPer-weekday windows keyed mon…sun; at least one day. A day that is not a key has no window and is closed.
windows.member.monobject (CampaignDayWindow)noOne 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.startstringyes
windows.member.mon.endstringyes
windows.member.tueobject (CampaignDayWindow)noOne 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.startstringyes
windows.member.tue.endstringyes
windows.member.wedobject (CampaignDayWindow)noOne 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.startstringyes
windows.member.wed.endstringyes
windows.member.thuobject (CampaignDayWindow)noOne 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.startstringyes
windows.member.thu.endstringyes
windows.member.friobject (CampaignDayWindow)noOne 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.startstringyes
windows.member.fri.endstringyes
windows.member.satobject (CampaignDayWindow)noOne 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.startstringyes
windows.member.sat.endstringyes
windows.member.sunobject (CampaignDayWindow)noOne 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.startstringyes
windows.member.sun.endstringyes
windows.account_adminobject (CampaignWeeklyWindow)noPer-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.monobject (CampaignDayWindow)noOne 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.startstringyes
windows.account_admin.mon.endstringyes
windows.account_admin.tueobject (CampaignDayWindow)noOne 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.startstringyes
windows.account_admin.tue.endstringyes
windows.account_admin.wedobject (CampaignDayWindow)noOne 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.startstringyes
windows.account_admin.wed.endstringyes
windows.account_admin.thuobject (CampaignDayWindow)noOne 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.startstringyes
windows.account_admin.thu.endstringyes
windows.account_admin.friobject (CampaignDayWindow)noOne 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.startstringyes
windows.account_admin.fri.endstringyes
windows.account_admin.satobject (CampaignDayWindow)noOne 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.startstringyes
windows.account_admin.sat.endstringyes
windows.account_admin.sunobject (CampaignDayWindow)noOne 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.startstringyes
windows.account_admin.sun.endstringyes
timezonestring,nullnoAn IANA zone; required when windows is not empty, refused otherwise.
reasonstring,nullno
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.

FieldTypeRequiredDescription
login_policyobject (TenantLoginPolicyVersion)yesOne published (append-only) version of the tenant's login policy.
login_policy.versionintegeryes
login_policy.modeoff · observe · enforceyes
login_policy.ip_allowarray of stringyes
login_policy.windowsobjectyes
login_policy.windows.memberobject (CampaignWeeklyWindow)noPer-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.monobject (CampaignDayWindow)noOne 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.tueobject (CampaignDayWindow)noOne 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.wedobject (CampaignDayWindow)noOne 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.thuobject (CampaignDayWindow)noOne 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.friobject (CampaignDayWindow)noOne 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.satobject (CampaignDayWindow)noOne 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.sunobject (CampaignDayWindow)noOne 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_adminobject (CampaignWeeklyWindow)noPer-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.monobject (CampaignDayWindow)noOne 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.tueobject (CampaignDayWindow)noOne 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.wedobject (CampaignDayWindow)noOne 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.thuobject (CampaignDayWindow)noOne 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.friobject (CampaignDayWindow)noOne 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.satobject (CampaignDayWindow)noOne 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.sunobject (CampaignDayWindow)noOne 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.timezonestring,nullyes
login_policy.set_bystringyes
login_policy.reasonstring,nullyes
login_policy.inserted_atstring (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/history

Newest 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

NameInTypeRequiredDescription
limitqueryintegernoPage size (a value over the ceiling is capped, not refused).
cursorqueryintegernoThe next_cursor of the previous page.

Responses

200 ​

A page of versions.

FieldTypeRequiredDescription
versionsarray of TenantLoginPolicyVersionyes
versions[].versionintegeryes
versions[].modeoff · observe · enforceyes
versions[].ip_allowarray of stringyes
versions[].windowsobjectyes
versions[].windows.memberobject (CampaignWeeklyWindow)noPer-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.monobject (CampaignDayWindow)noOne 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.tueobject (CampaignDayWindow)noOne 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.wedobject (CampaignDayWindow)noOne 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.thuobject (CampaignDayWindow)noOne 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.friobject (CampaignDayWindow)noOne 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.satobject (CampaignDayWindow)noOne 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.sunobject (CampaignDayWindow)noOne 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_adminobject (CampaignWeeklyWindow)noPer-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.monobject (CampaignDayWindow)noOne 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.tueobject (CampaignDayWindow)noOne 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.wedobject (CampaignDayWindow)noOne 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.thuobject (CampaignDayWindow)noOne 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.friobject (CampaignDayWindow)noOne 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.satobject (CampaignDayWindow)noOne 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.sunobject (CampaignDayWindow)noOne 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[].timezonestring,nullyes
versions[].set_bystringyes
versions[].reasonstring,nullyes
versions[].inserted_atstring (date-time)yes
next_cursorinteger,nullyesPass 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/tenants

Docs parity: API keys are tenant-scoped, so the list contains exactly the authenticated tenant.

Responses

200 ​

One-element list (own tenant).

FieldTypeRequiredDescription
tenantsarray of Tenantyes
tenants[].idstring (uuid)no
tenants[].namestringno
tenants[].account_idstring (uuid)noThe EMPRESA this tenant (sede) belongs to (DA-2).
tenants[].billing_account_idstring (uuid)noThe billing account the sede is invoiced under (DA-3). Shared by every sede of a consolidated empresa; one per sede when per_site.
tenants[].statusactive · suspendedno
tenants[].syntheticbooleannotrue 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_monthsintegerno
tenants[].seatsintegerno
tenants[].tierstarter · growth · scale · enterprise · dialer_core · compliance_pro · audit_shieldno
tenants[].voice_ai_enabledbooleanno
tenants[].ai_monthly_budget_usdnumber | nullnoCost-integrity ceiling (admin-set; null = no cap).
tenants[].ai_max_call_secondsinteger | nullnoPer-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_effectiveintegernoThe 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_usdnumber | nullnoMonth-to-date SMS spend cap.
tenants[].human_initiated_enabledbooleannoD17 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_cpsinteger | nullnoPer-tenant CPS ceiling (admin-set; null = no ceiling). ADR #12.
tenants[].max_concurrent_channelsinteger | nullnoPer-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_namestring | nullnoSeller name for the FTC TSR abandoned-call identification message (admin-set; null = not configured, predictive over-dial stays dark). ADR #14.
tenants[].abandon_seller_phonestring | nullnoE.164 callback number for the abandoned-call identification message (admin-set; null = not configured). ADR #14.
tenants[].tax_countrystring | nullnoISO-3166-1 alpha-2 country of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77.
tenants[].tax_regionstring | nullnoState/province code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77.
tenants[].tax_postal_codestring | nullnoPostal code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77.
tenants[].kyc_verified_atstring (date-time) | nullnoWhen 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_atstring (date-time)no
tenants[].updated_atstring (date-time)no

401 — Unauthorized ​

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

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

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

Responses

200 ​

The tenant.

FieldTypeRequiredDescription
tenantobject (Tenant)yes
tenant.idstring (uuid)no
tenant.namestringno
tenant.account_idstring (uuid)noThe EMPRESA this tenant (sede) belongs to (DA-2).
tenant.billing_account_idstring (uuid)noThe billing account the sede is invoiced under (DA-3). Shared by every sede of a consolidated empresa; one per sede when per_site.
tenant.statusactive · suspendedno
tenant.syntheticbooleannotrue 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_monthsintegerno
tenant.seatsintegerno
tenant.tierstarter · growth · scale · enterprise · dialer_core · compliance_pro · audit_shieldno
tenant.voice_ai_enabledbooleanno
tenant.ai_monthly_budget_usdnumber | nullnoCost-integrity ceiling (admin-set; null = no cap).
tenant.ai_max_call_secondsinteger | nullnoPer-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_effectiveintegernoThe 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_usdnumber | nullnoMonth-to-date SMS spend cap.
tenant.human_initiated_enabledbooleannoD17 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_cpsinteger | nullnoPer-tenant CPS ceiling (admin-set; null = no ceiling). ADR #12.
tenant.max_concurrent_channelsinteger | nullnoPer-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_namestring | nullnoSeller name for the FTC TSR abandoned-call identification message (admin-set; null = not configured, predictive over-dial stays dark). ADR #14.
tenant.abandon_seller_phonestring | nullnoE.164 callback number for the abandoned-call identification message (admin-set; null = not configured). ADR #14.
tenant.tax_countrystring | nullnoISO-3166-1 alpha-2 country of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77.
tenant.tax_regionstring | nullnoState/province code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77.
tenant.tax_postal_codestring | nullnoPostal code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77.
tenant.kyc_verified_atstring (date-time) | nullnoWhen 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_atstring (date-time)no
tenant.updated_atstring (date-time)no

401 — Unauthorized ​

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

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

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