Appearance
Sso
Generated from
DialerDigital/corecommit0fc18008b3c8— specopenapi/dialer-v1.yamlv0.134.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.
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
| Method | Path | Summary |
|---|---|---|
GET | /v1/auth/sso/{tenant_id}/metadata | SAML service-provider metadata of a tenant (for its IdP admin) |
GET | /v1/auth/sso/{tenant_id}/slo | SAML Single Logout, from the IdP (HTTP-Redirect binding) |
POST | /v1/auth/sso/{tenant_id}/slo | SAML Single Logout, from the IdP (HTTP-POST binding) |
POST | /v1/auth/sso/start | Start a SAML SSO login (SP-initiated) |
POST | /v1/auth/sso/{tenant_id}/acs | SAML Assertion Consumer Service (HTTP-POST binding) |
POST | /v1/auth/sso/exchange | Redeem an SSO login code for a session |
GET | /v1/sso/idp-config | The tenant's SAML identity-provider configuration |
PUT | /v1/sso/idp-config | Load or replace the tenant's SAML identity-provider configuration |
PATCH | /v1/sso/idp-config | Enable or disable the tenant's SAML configuration |
DELETE | /v1/sso/idp-config | Remove the tenant's SAML configuration |
GET | /v1/sso/idp-config/events | Audit 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}/metadataThe 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id | path | string (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}/sloSingle 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id | path | string (uuid) | yes | |
SAMLRequest | query | string | no | |
RelayState | query | string | no | |
SigAlg | query | string | no | |
Signature | query | string | no |
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}/sloSingle 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id | path | string (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/startReturns 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)
| Field | Type | Required | Description |
|---|---|---|---|
tenant_id | string (uuid) | yes |
Responses
200
The IdP URL and the secret of this browser (Cache-Control: no-store).
| Field | Type | Required | Description |
|---|---|---|---|
redirect_url | string (uri) | yes | |
browser_secret | string | yes | Shown exactly once; core stores nothing at start and re-derives it at the ACS to keep only its hash. |
expires_at | string (date-time) | yes |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}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}/acsWhere 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
tenant_id | path | string (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/exchangeRedeems 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)
| Field | Type | Required | Description |
|---|---|---|---|
code | string | yes | |
browser_secret | string | yes |
Responses
200
The session (token is the only time the plaintext is visible).
| Field | Type | Required | Description |
|---|---|---|---|
token | string | yes | |
session | object (UserSession) | yes | A ddu_ login session — the token hash is never exposed. |
session.id | string (uuid) | no | |
session.user_id | string (uuid) | no | |
session.expires_at | string (date-time) | no | Fixed at login (no sliding renew); 24h engine ceiling. |
session.revoked_at | string (date-time) | null | no | |
session.created_at | string (date-time) | no | |
user | object (User) | yes | A dashboard user — the password hash is never exposed. |
user.id | string (uuid) | no | |
user.email | string (email) | no | Global login identifier, stored lowercase. |
user.name | string | no | |
user.status | active · disabled | no | |
user.role | member · account_admin | no | Intra-account privilege. Only an account_admin may create or modify the empresa-wide rows every sibling sede inherits (scope: "empresa" on carriers and SMS providers); a member gets 403. Read-only here: promotion is an operator gesture, never a request. |
user.password_updated_at | string (date-time) | no | |
user.last_login_at | string (date-time) | null | no | |
user.mfa_enabled | boolean | no | Whether the person has an ACTIVE second factor (an enrolment still pending confirmation is false). Emitted ONLY when the caller is an account_admin dashboard login — the one who can act on it with POST /v1/users/{id}/mfa/remove; for a member, a dd_ key, or when the caller's role cannot be determined the field is ABSENT (not false). Wherever a User is rendered, the same rule holds: GET /v1/users, GET /v1/me, the user returned by DELETE /v1/users/{id} and POST /v1/users/{id}/enable, and the user in the session responses (POST /v1/auth/login, POST /v1/auth/sso/exchange, and the two MFA completion routes) — there, for the account_admin who just signed in. |
user.created_at | string (date-time) | no | |
user.updated_at | string (date-time) | no | |
tenant | object (Tenant) | yes | |
tenant.id | string (uuid) | no | |
tenant.name | string | no | |
tenant.account_id | string (uuid) | no | The EMPRESA this tenant (sede) belongs to (DA-2). |
tenant.billing_account_id | string (uuid) | no | The billing account the sede is invoiced under (DA-3). Shared by every sede of a consolidated empresa; one per sede when per_site. |
tenant.status | active · suspended | no | |
tenant.synthetic | boolean | no | true for a demonstration tenant. It originates no traffic: manual dial, SMS send, the external leg of a transfer and the sandbox answer 403 synthetic_tenant. Always present, never null, so a console can warn BEFORE the action fails (core#2512). |
tenant.retention_months | integer | no | |
tenant.seats | integer | no | |
tenant.tier | starter · growth · scale · enterprise · dialer_core · compliance_pro · audit_shield | no | |
tenant.voice_ai_enabled | boolean | no | |
tenant.ai_monthly_budget_usd | number | null | no | Cost-integrity ceiling (admin-set; null = no cap). |
tenant.ai_max_call_seconds | integer | null | no | Per-AI-call hard duration ceiling (seconds) set by the admin. null means the PLATFORM default applies — never "no ceiling" (core#922, ADR #115). |
tenant.ai_max_call_seconds_effective | integer | no | The ceiling FreeSWITCH is actually handed for this tenant's AI legs: ai_max_call_seconds when set, else the cell's platform default (AI_MAX_CALL_SECONDS_DEFAULT, compiled fallback 900). Always present, never null. Read-only (derived). |
tenant.sms_monthly_budget_usd | number | null | no | Month-to-date SMS spend cap. |
tenant.human_initiated_enabled | boolean | no | D17 human-initiated consent posture (admin-set; default false). When true, a LIVE preview/manual/CTI voice dial with no artificial and no AI voice does not require prior express consent (no ATDS). Every other gate — DNC, quiet hours, Reg F, state matrix, artificial voice — still applies unchanged. |
tenant.max_cps | integer | null | no | Per-tenant CPS ceiling (admin-set; null = no ceiling). ADR #12. |
tenant.max_concurrent_channels | integer | null | no | Per-tenant ceiling of concurrent calls, one unit per call with or without an agent (admin-set; null = no ceiling). ADR #12, ADR #185. |
tenant.abandon_seller_name | string | null | no | Seller name for the FTC TSR abandoned-call identification message (admin-set; null = not configured, predictive over-dial stays dark). ADR #14. |
tenant.abandon_seller_phone | string | null | no | E.164 callback number for the abandoned-call identification message (admin-set; null = not configured). ADR #14. |
tenant.tax_country | string | null | no | ISO-3166-1 alpha-2 country of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77. |
tenant.tax_region | string | null | no | State/province code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77. |
tenant.tax_postal_code | string | null | no | Postal code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77. |
tenant.kyc_verified_at | string (date-time) | null | no | When the platform verified this customer (admin-set; null = not yet). A 1-seat account that is not verified cannot create or start a campaign that runs without an agent (403 verification_required, ADR #279). |
tenant.created_at | string (date-time) | no | |
tenant.updated_at | string (date-time) | no |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401
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-configThe 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.
| Field | Type | Required | Description |
|---|---|---|---|
sso_idp_config | SsoIdpConfig | null | yes | |
service_provider | SsoServiceProvider | null | yes |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403
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-configFull-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)
| Field | Type | Required | Description |
|---|---|---|---|
idp_entity_id | string | yes | |
sso_url | string | yes | |
slo_url | string,null | no | The IdP's SingleLogoutService (https); absent or null means the IdP publishes none. |
certificates | array of string | yes | |
require_signed_assertion | boolean | no | |
require_signed_response | boolean | no | |
email_attribute | string,null | no | SAML attribute that carries the email; absent or null means the NameID. |
enabled | boolean | no |
Responses
200
Stored.
| Field | Type | Required | Description |
|---|---|---|---|
sso_idp_config | SsoIdpConfig | null | yes | |
service_provider | SsoServiceProvider | null | yes |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403
role_forbidden: 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-configOnly 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)
| Field | Type | Required | Description |
|---|---|---|---|
enabled | boolean | yes |
Responses
200
The configuration with its new state.
| Field | Type | Required | Description |
|---|---|---|---|
sso_idp_config | SsoIdpConfig | null | yes | |
service_provider | SsoServiceProvider | null | yes |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403
role_forbidden: 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-configRemoves 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.
| Field | Type | Required | Description |
|---|---|---|---|
sso_idp_config | object (SsoIdpConfig) | yes | |
sso_idp_config.id | string (uuid) | yes | |
sso_idp_config.idp_entity_id | string | yes | |
sso_idp_config.sso_url | string | yes | |
sso_idp_config.slo_url | string,null | yes | |
sso_idp_config.certificates | array of object | yes | |
sso_idp_config.certificates[].sha256 | string | yes | Lowercase hex SHA-256 of the certificate DER; what a signature is pinned to. |
sso_idp_config.certificates[].not_before | string,null (date-time) | yes | |
sso_idp_config.certificates[].not_after | string,null (date-time) | yes | |
sso_idp_config.require_signed_assertion | boolean | yes | |
sso_idp_config.require_signed_response | boolean | yes | |
sso_idp_config.email_attribute | string,null | yes | |
sso_idp_config.enabled | boolean | yes | |
sso_idp_config.updated_by | string | yes | |
sso_idp_config.created_at | string (date-time) | yes | |
sso_idp_config.updated_at | string (date-time) | yes |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403
role_forbidden: 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/eventsThe 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.
| Field | Type | Required | Description |
|---|---|---|---|
events | array of SsoIdpConfigEvent | yes | |
events[].id | string (uuid) | yes | |
events[].action | created · updated · enabled · disabled · deleted | yes | |
events[].actor | string | yes | |
events[].snapshot | object | yes | |
events[].occurred_at | string (date-time) | yes |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403
role_forbidden: 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"
}
}