Skip to content

Sso ​

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

SAML 2.0 single sign-on per tenant: the tenant's identity-provider configuration (loaded by an account_admin, audited append-only) and the service-provider metadata its IdP admin needs. SP-initiated only; a person must already exist in the tenant (no just-in-time provisioning).

Endpoints ​

MethodPathSummary
GET/v1/auth/sso/{tenant_id}/metadataSAML service-provider metadata of a tenant (for its IdP admin)
GET/v1/auth/sso/{tenant_id}/sloSAML Single Logout, from the IdP (HTTP-Redirect binding)
POST/v1/auth/sso/{tenant_id}/sloSAML Single Logout, from the IdP (HTTP-POST binding)
POST/v1/auth/sso/startStart a SAML SSO login (SP-initiated)
POST/v1/auth/sso/{tenant_id}/acsSAML Assertion Consumer Service (HTTP-POST binding)
POST/v1/auth/sso/exchangeRedeem an SSO login code for a session
GET/v1/sso/idp-configThe tenant's SAML identity-provider configuration
PUT/v1/sso/idp-configLoad or replace the tenant's SAML identity-provider configuration
PATCH/v1/sso/idp-configEnable or disable the tenant's SAML configuration
DELETE/v1/sso/idp-configRemove the tenant's SAML configuration
GET/v1/sso/idp-config/eventsAudit trail of the tenant's SAML configuration

SAML service-provider metadata of a tenant (for its IdP admin) ​

http
GET /v1/auth/sso/{tenant_id}/metadata

The SAML 2.0 SP metadata the tenant's IdP admin pastes into the identity provider: this tenant's entityID, its Assertion Consumer Service (HTTP-POST), its SingleLogoutService (HTTP-POST and HTTP-Redirect) and the NameID format (emailAddress). PUBLIC (security: []) and built from the tenant_id in the path alone — it reads nothing per tenant, so the metadata of a tenant that does not exist is byte-for-byte that of one that does (no tenant-existence oracle). Every tenant is a DISTINCT SP: an assertion issued for one tenant's SP never validates for another's. 404 while the cell has no public SP base URL configured (SSO_SP_BASE_URL), and for a tenant_id that is not a UUID.

Auth: none — authenticated by request signature (see below).

Parameters

NameInTypeRequiredDescription
tenant_idpathstring (uuid)yes

Responses

200 ​

The SP metadata document.

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

SAML Single Logout, from the IdP (HTTP-Redirect binding) ​

http
GET /v1/auth/sso/{tenant_id}/slo

Single Logout (core#1581, ADR #210). Where the tenant's IdP sends a LogoutRequest through the browser — HTTP-Redirect (GET, SAMLRequest in the query, the signature in Signature/SigAlg, verified over the RAW query) or HTTP-POST (SAMLRequest form field, enveloped signature). When the request is signed by THIS tenant's IdP, every live ddu_ session of the person its NameID names is revoked.

Validated as strictly as the ACS: signature against the pinned certificates of this tenant only, Issuer, Destination (this endpoint), IssueInstant within a few minutes, NotOnOrAfter, and a LogoutRequestID never seen before (replay). Fail-closed: ANY doubt — unknown tenant, SSO off, bad signature, foreign issuer, expired, repeated — is ONE identical 400 and nothing is revoked; the reason is only in the server log. A NameID that names nobody is the same 303 as one that does (no user enumeration). It never answers 500 for a hostile document.

Success is 303: to the IdP's slo_url carrying a LogoutResponse (unsigned; the RelayState echoed when printable and at most 80 bytes) or, when the IdP published none, to the dashboard. A SAMLResponse (the IdP answering a logout core started) changes nothing and is 303 to the dashboard. 404 while the cell has no public SP base URL.

IdP-INITIATED LOGIN (an unsolicited Response at the ACS) is deliberately NOT supported: it would reopen login-CSRF and assertion replay, which the two-step exchange closes.

Auth: none — authenticated by request signature (see below).

Parameters

NameInTypeRequiredDescription
tenant_idpathstring (uuid)yes
SAMLRequestquerystringno
RelayStatequerystringno
SigAlgquerystringno
Signaturequerystringno

Responses

303 ​

Done (or nothing to do); Location is the IdP's slo_url with the LogoutResponse, or the dashboard.

400 ​

The logout request was rejected (one answer for every reason).

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 — LoginRateLimited ​

The per-ACCOUNT login brake refused this attempt (fixed window; retryable later). code rate_limited. Keyed per email (/v1/auth/login) or per {tenant_id, email} (/v1/auth/agent-login), so it bounds many passwords against ONE account. Credential spraying across many accounts is currently COUNTED AND ALERTED but not refused, because the only per-source key available to core is collapsed by the dashboard's reverse proxy — a spraying attempt therefore still returns the ordinary 401. Retry-After is the time left in the fixed window.

/v1/auth/login has a second, CUMULATIVE reason for the same 429 (core#2335, NIST SP 800-63B-4 §3.2.2): once a person accumulates login_progressive_threshold_failures (default 20) wrong passwords since their last successful sign-in, inside 24 h, the login is held even for the CORRECT password for 15 min after the last failure, doubling for every further threshold-many failures (capped at 6 h); Retry-After is the time left. A pause, never a permanent lock. Held attempts are not recorded, so they do not lengthen it.

json
{
  "error": {
    "code": "rate_limited",
    "message": "too many login attempts; retry later"
  }
}

SAML Single Logout, from the IdP (HTTP-POST binding) ​

http
POST /v1/auth/sso/{tenant_id}/slo

Single Logout (core#1581, ADR #210). Where the tenant's IdP sends a LogoutRequest through the browser — HTTP-Redirect (GET, SAMLRequest in the query, the signature in Signature/SigAlg, verified over the RAW query) or HTTP-POST (SAMLRequest form field, enveloped signature). When the request is signed by THIS tenant's IdP, every live ddu_ session of the person its NameID names is revoked.

Validated as strictly as the ACS: signature against the pinned certificates of this tenant only, Issuer, Destination (this endpoint), IssueInstant within a few minutes, NotOnOrAfter, and a LogoutRequestID never seen before (replay). Fail-closed: ANY doubt — unknown tenant, SSO off, bad signature, foreign issuer, expired, repeated — is ONE identical 400 and nothing is revoked; the reason is only in the server log. A NameID that names nobody is the same 303 as one that does (no user enumeration). It never answers 500 for a hostile document.

Success is 303: to the IdP's slo_url carrying a LogoutResponse (unsigned; the RelayState echoed when printable and at most 80 bytes) or, when the IdP published none, to the dashboard. A SAMLResponse (the IdP answering a logout core started) changes nothing and is 303 to the dashboard. 404 while the cell has no public SP base URL.

IdP-INITIATED LOGIN (an unsolicited Response at the ACS) is deliberately NOT supported: it would reopen login-CSRF and assertion replay, which the two-step exchange closes.

Auth: none — authenticated by request signature (see below).

Parameters

NameInTypeRequiredDescription
tenant_idpathstring (uuid)yes

Responses

303 ​

Done (or nothing to do); Location is the IdP's slo_url with the LogoutResponse, or the dashboard.

400 ​

The logout request was rejected (one answer for every reason).

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 — LoginRateLimited ​

The per-ACCOUNT login brake refused this attempt (fixed window; retryable later). code rate_limited. Keyed per email (/v1/auth/login) or per {tenant_id, email} (/v1/auth/agent-login), so it bounds many passwords against ONE account. Credential spraying across many accounts is currently COUNTED AND ALERTED but not refused, because the only per-source key available to core is collapsed by the dashboard's reverse proxy — a spraying attempt therefore still returns the ordinary 401. Retry-After is the time left in the fixed window.

/v1/auth/login has a second, CUMULATIVE reason for the same 429 (core#2335, NIST SP 800-63B-4 §3.2.2): once a person accumulates login_progressive_threshold_failures (default 20) wrong passwords since their last successful sign-in, inside 24 h, the login is held even for the CORRECT password for 15 min after the last failure, doubling for every further threshold-many failures (capped at 6 h); Retry-After is the time left. A pause, never a permanent lock. Held attempts are not recorded, so they do not lengthen it.

json
{
  "error": {
    "code": "rate_limited",
    "message": "too many login attempts; retry later"
  }
}

Start a SAML SSO login (SP-initiated) ​

http
POST /v1/auth/sso/start

Returns the URL of the tenant's identity provider carrying a fresh AuthnRequest (HTTP-Redirect binding, unsigned), plus a browser_secret. It writes NOTHING: the login state travels in the RelayState, a compact HMAC-signed token (request id, expiry, IdP configuration version; the tenant is authenticated inside the MAC and the browser secret is derived, never carried) that the ACS verifies. The dashboard sends the browser to redirect_url and keeps browser_secret in the SAME browser (sessionStorage): it is the only thing that can redeem the login later (POST /v1/auth/sso/exchange), so a login a third party starts cannot be finished in someone else's browser. The login lives 5 minutes and is single-use (spent when a validly signed Response for it reaches the ACS). PUBLIC; 404 is ONE answer for a tenant that does not exist, a tenant whose IdP is not ENABLED and a cell without SSO_SP_BASE_URL. The per-origin brake is OBSERVATION-ONLY by default (anonymous and stateless, and behind the dashboard proxy the origin collapses, so an enforced cap would deny every tenant at once); a cell that trusts its origin axis promotes it with BRAKE_ENFORCE_BUCKETS=sso_start.

Auth: none — authenticated by request signature (see below).

Request body (JSON, required)

FieldTypeRequiredDescription
tenant_idstring (uuid)yes

Responses

200 ​

The IdP URL and the secret of this browser (Cache-Control: no-store).

FieldTypeRequiredDescription
redirect_urlstring (uri)yes
browser_secretstringyesShown exactly once; core stores nothing at start and re-derives it at the ACS to keep only its hash.
expires_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"
  }
}

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 ​

Only in a cell that PROMOTED the start brake (BRAKE_ENFORCE_BUCKETS lists sso_start): the per-ORIGIN brake refused this call (rate_limited; 300 per fixed window by default, SSO_START_MAX_PER_WINDOW). Never in the default observation-only mode. Keyed by the caller's origin, never by tenant_id. Retry-After is the time left in the window.

SAML Assertion Consumer Service (HTTP-POST binding) ​

http
POST /v1/auth/sso/{tenant_id}/acs

Where the IdP's browser POSTs the SAMLResponse (form-encoded, with the RelayState returned by POST /v1/auth/sso/start). It never answers with a session: it is ALWAYS a 303 back to the dashboard callback (/sso/callback), with #code=<one-shot code> in the URL FRAGMENT on success (single use, valid 2 minutes, redeemed at POST /v1/auth/sso/exchange) or #error=sso_failed on ANY rejection — the precise reason is only in the server log, never in the answer.

The response is validated fail-closed: the signature (assertion and/or response, per the tenant's configuration) against the pinned certificates of THIS tenant only (the KeyInfo of the document is ignored), the signed element is the one whose claims are read (no XML signature wrapping), a single assertion, Issuer, Audience (this tenant's SP), Recipient/Destination (this ACS), InResponseTo (the request core issued, verified from the signed RelayState and spent only when the IdP signature validates: a forged Response does not burn the login), NotBefore/NotOnOrAfter with 60 s of skew, and an assertion ID never seen before (replay). The person must ALREADY exist (matched by the verified email; there is no just-in-time provisioning) and be active.

Auth: none — authenticated by request signature (see below).

Parameters

NameInTypeRequiredDescription
tenant_idpathstring (uuid)yes

Responses

303 ​

Redirect to the dashboard callback; Location carries #code=... or #error=sso_failed in its fragment.

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

Redeem an SSO login code for a session ​

http
POST /v1/auth/sso/exchange

Redeems the code the ACS handed to the dashboard together with the browser_secret POST /v1/auth/sso/start returned to this same browser, and mints the usual ddu_ session (same body as POST /v1/auth/login). The code is single-use and dies on its FIRST presentation, whether the secret matched or not; unknown, expired, spent and wrong-browser are ONE identical 401. An SSO login does not ask for the application's own second factor (the IdP is the authority), and works whether or not the tenant closed the password lane (sso_required_enforced). The tenant's login policy is asked here (the IdP's word is not the tenant's allowlist): a tenant that published a login policy (allowed addresses and hours per role, ADR 227) in mode enforce refuses a sign-in from an address outside it or at an hour outside it with ONE 403 login_policy_denied, the same for the address and the hour: which of them failed is told only by the tenant's sign-in trail (GET /v1/auth/sign-in-events). Asked AFTER the credential proved right and before anything is issued. With the policy off or absent the sign-in is the one of before.

Auth: none — authenticated by request signature (see below).

Request body (JSON, required)

FieldTypeRequiredDescription
codestringyes
browser_secretstringyes

Responses

200 ​

The session (token is the only time the plaintext is visible).

FieldTypeRequiredDescription
tokenstringyes
sessionobject (UserSession)yesA ddu_ login session — the token hash is never exposed.
session.idstring (uuid)no
session.user_idstring (uuid)no
session.expires_atstring (date-time)noFixed at login (no sliding renew); 24h engine ceiling.
session.revoked_atstring (date-time) | nullno
session.created_atstring (date-time)no
userobject (User)yesA 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
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 ​

Unknown, expired, already-used or wrong-browser code.

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 — LoginRateLimited ​

The per-ACCOUNT login brake refused this attempt (fixed window; retryable later). code rate_limited. Keyed per email (/v1/auth/login) or per {tenant_id, email} (/v1/auth/agent-login), so it bounds many passwords against ONE account. Credential spraying across many accounts is currently COUNTED AND ALERTED but not refused, because the only per-source key available to core is collapsed by the dashboard's reverse proxy — a spraying attempt therefore still returns the ordinary 401. Retry-After is the time left in the fixed window.

/v1/auth/login has a second, CUMULATIVE reason for the same 429 (core#2335, NIST SP 800-63B-4 §3.2.2): once a person accumulates login_progressive_threshold_failures (default 20) wrong passwords since their last successful sign-in, inside 24 h, the login is held even for the CORRECT password for 15 min after the last failure, doubling for every further threshold-many failures (capped at 6 h); Retry-After is the time left. A pause, never a permanent lock. Held attempts are not recorded, so they do not lengthen it.

json
{
  "error": {
    "code": "rate_limited",
    "message": "too many login attempts; retry later"
  }
}

The tenant's SAML identity-provider configuration ​

http
GET /v1/sso/idp-config

The tenant's IdP configuration (sso_idp_config, null when none is loaded) and the service-provider URLs to give the IdP (service_provider, null while the cell has no SP base URL). The certificates are shown as SHA-256 fingerprint + validity window, never as PEM.

account_admin dashboard login only (see 403).

Responses

200 ​

The configuration and the SP URLs.

FieldTypeRequiredDescription
sso_idp_configSsoIdpConfig | nullyes
service_providerSsoServiceProvider | 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 ​

role_forbidden: anything but a dashboard login (ddu_) whose person has the account_admin role — a dd_ API key and a member login included; forbidden: a widget token (ddw_) or a suspended tenant.

json
{
  "error": {
    "code": "role_forbidden",
    "message": "SSO configuration takes a dashboard login with the account_admin role"
  }
}

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

Load or replace the tenant's SAML identity-provider configuration ​

http
PUT /v1/sso/idp-config

Full-body upsert: the IdP's entityID, its HTTP-Redirect SSO URL (https only) and one or two X.509 signing certificates (two to rotate without cutting access). Each certificate must be exactly one PEM block, RSA of at least 2048 bits, and valid now. A certificate is trusted by its SHA-256 fingerprint (the chain is not validated). At least one of require_signed_assertion / require_signed_response must be true: there is no configuration that accepts an unsigned assertion.

The configuration is stored DISABLED unless enabled: true is sent; loading it does not open the door. Every effective change writes an audit fact, in the same transaction, with the person who made it. A replacement identical to what is stored writes nothing.

A value that fails validation (a non-https URL, a certificate that is weak, expired or not exactly one PEM block, a repeated certificate) is 400 naming the field.

account_admin dashboard login only (see 403).

Request body (JSON, required)

FieldTypeRequiredDescription
idp_entity_idstringyes
sso_urlstringyes
slo_urlstring,nullnoThe IdP's SingleLogoutService (https); absent or null means the IdP publishes none.
certificatesarray of stringyes
require_signed_assertionbooleanno
require_signed_responsebooleanno
email_attributestring,nullnoSAML attribute that carries the email; absent or null means the NameID.
enabledbooleanno

Responses

200 ​

Stored.

FieldTypeRequiredDescription
sso_idp_configSsoIdpConfig | nullyes
service_providerSsoServiceProvider | 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: anything but a dashboard login (ddu_) whose person has the account_admin role — a dd_ API key and a member login included; forbidden: a widget token (ddw_) or a suspended tenant.

json
{
  "error": {
    "code": "role_forbidden",
    "message": "SSO configuration takes a dashboard login with the account_admin role"
  }
}

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

Enable or disable the tenant's SAML configuration ​

http
PATCH /v1/sso/idp-config

Only enabled is accepted. Enabling re-checks that every stored certificate is still valid now (422 otherwise). 404 when the tenant has no configuration. Setting the value it already has writes nothing.

account_admin dashboard login only (see 403).

Request body (JSON, required)

FieldTypeRequiredDescription
enabledbooleanyes

Responses

200 ​

The configuration with its new state.

FieldTypeRequiredDescription
sso_idp_configSsoIdpConfig | nullyes
service_providerSsoServiceProvider | 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: anything but a dashboard login (ddu_) whose person has the account_admin role — a dd_ API key and a member login included; forbidden: a widget token (ddw_) or a suspended tenant.

json
{
  "error": {
    "code": "role_forbidden",
    "message": "SSO configuration takes a dashboard login with the account_admin role"
  }
}

404 — NotFound ​

Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

422 — Unprocessable ​

Shape is fine, semantics are not (broken domain rule).

json
{
  "error": {
    "code": "unprocessable",
    "message": "scheduled_at is closer than the minimum notice for a callback (10 minutes unless the tenant's callback_min_notice_seconds says otherwise)"
  }
}

429 — ApiKeyRateLimited ​

This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (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"
  }
}

Remove the tenant's SAML configuration ​

http
DELETE /v1/sso/idp-config

Removes the configuration; its audit history stays. The answer is the configuration that was removed. 404 when there is none.

account_admin dashboard login only (see 403).

Responses

200 ​

Removed.

FieldTypeRequiredDescription
sso_idp_configobject (SsoIdpConfig)yes
sso_idp_config.idstring (uuid)yes
sso_idp_config.idp_entity_idstringyes
sso_idp_config.sso_urlstringyes
sso_idp_config.slo_urlstring,nullyes
sso_idp_config.certificatesarray of objectyes
sso_idp_config.certificates[].sha256stringyesLowercase hex SHA-256 of the certificate DER; what a signature is pinned to.
sso_idp_config.certificates[].not_beforestring,null (date-time)yes
sso_idp_config.certificates[].not_afterstring,null (date-time)yes
sso_idp_config.require_signed_assertionbooleanyes
sso_idp_config.require_signed_responsebooleanyes
sso_idp_config.email_attributestring,nullyes
sso_idp_config.enabledbooleanyes
sso_idp_config.updated_bystringyes
sso_idp_config.created_atstring (date-time)yes
sso_idp_config.updated_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: anything but a dashboard login (ddu_) whose person has the account_admin role — a dd_ API key and a member login included; forbidden: a widget token (ddw_) or a suspended tenant.

json
{
  "error": {
    "code": "role_forbidden",
    "message": "SSO configuration takes a dashboard login with the account_admin role"
  }
}

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

Audit trail of the tenant's SAML configuration ​

http
GET /v1/sso/idp-config/events

The append-only facts (created, updated, enabled, disabled, deleted), newest first, at most 100. Each carries who did it and a snapshot with certificate fingerprints (never PEM).

account_admin dashboard login only (see 403).

Responses

200 ​

The facts.

FieldTypeRequiredDescription
eventsarray of SsoIdpConfigEventyes
events[].idstring (uuid)yes
events[].actioncreated · updated · enabled · disabled · deletedyes
events[].actorstringyes
events[].snapshotobjectyes
events[].occurred_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: anything but a dashboard login (ddu_) whose person has the account_admin role — a dd_ API key and a member login included; forbidden: a widget token (ddw_) or a suspended tenant.

json
{
  "error": {
    "code": "role_forbidden",
    "message": "SSO configuration takes a dashboard login with the account_admin role"
  }
}

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.