SenseCrypt Docs
Reference

Error codes

The complete SenseCrypt error taxonomy — OAuth/OIDC, SAML, SCIM, and device-plane error codes, their HTTP statuses, the exact envelopes, and what each one means.

This page catalogs the errors SenseCrypt returns across its four protocol surfaces. Many authentication failures are deliberately opaque to prevent enumeration: treat auth failures as opaque, and branch only on the documented codes below — never on the exact wording of a message.

Error envelopes

SenseCrypt uses four distinct error shapes depending on the surface:

OAuth/OIDC protocol envelope — RFC 6749 §5.2, used by the OIDC and SAML protocol endpoints:

{ "error": "invalid_grant", "error_description": "Code invalid, expired, or already used" }

error_description is included only when it adds detail beyond the code.

Management / device envelope — used by the device plane and the management routes, and by a rate-limit 429 on every plane but SCIM, which answers a throttle in its own envelope below (with Retry-After, so a provisioning connector backs off correctly). Where a throttle is decided on Accept — the browser plane, and /authorize and /logout — a browser gets the rate-limited card and a machine caller gets this envelope. Clients branch on error (and kind where present) and must treat unknown values as generic failures:

{ "error": "unauthorized", "message": "unauthorized", "kind": "replayed_request" }

On a 422 the management envelope also carries a details[] array naming each refused field and the rule it broke — see Management API validation errors below.

SCIM envelope — the SCIM-mandated shape:

{ "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], "status": "409", "scimType": "uniqueness", "detail": "a user with this userName or email already exists" }

Branded HTML page — the browser-facing endpoints have no RP to hand a machine envelope to, so they answer with a branded HTML card instead of JSON: the /authorize refusal page (400) and the logout confirmation, signed-out and bad-hint pages. The status-driven cards are narrower than that: they are served only on the browser plane — the paths under /v1/browser/ and /v1/link/ — and only when the request's Accept prefers HTML. There you get a uniform refusal card on a 401/403, an expired-link card on a 404, 405 or 422 (unbranded where the path matches no route at all), and a rate-limited card on a 429; there is no JSON body to parse, so branch on the HTTP status. Everywhere else a machine caller keeps the machine envelope, whatever the Accept header says — GET /v1/admin/oidc-apps with Accept: text/html and a stale session still answers 401 with {"error": "unauthorized", "message": …}, and a protocol path still answers the OAuth envelope. The endpoints a browser is sent to are the exception, each in its own way: /authorize's pre-redirect refusal is always the branded 400 page, while a rate-limit trip at /authorize or /logout — and every refusal of a SAML /sso request — answers a card only when Accept prefers HTML, and keeps the machine body otherwise (an RP backend, an SP backend and the conformance suites send */* or JSON, so they keep it). So parse the body on the management and protocol planes rather than branching on Accept.

A note on statuses: among OAuth errors, invalid_client is 401; every other OAuth error (invalid_grant, invalid_request, invalid_target, invalid_scope, unsupported_grant_type, unsupported_response_type, and the CIBA codes) is 400.


OAuth / OIDC errors

Token endpoint (POST /v1/idp/oidc/token)

CodeHTTPMeaning
unsupported_grant_type400grant_type isn't one of authorization_code, refresh_token, client_credentials, or CIBA.
invalid_grant400The authorization code is invalid, expired, or already used. This is deliberately opaque — a cross-tenant client and a failed access-gate check also collapse to this same message (see below).
invalid_grant400PKCE failure — a code_verifier is required but missing, fails the RFC 7636 format check, doesn't match the challenge, or was supplied when the session stored no challenge.
invalid_scope400A requested scope isn't granted to the client (re-checked here and on refresh).
invalid_grant400The authorization code is bound to a DPoP key (dpop_jkt, or a proof on the PAR push) and this request did not prove that same key. Checked before the code is consumed, so it cannot be burned by a party without the key.
invalid_request400The application registered tls_client_certificate_bound_access_tokens and the request carried no client certificate. The certificate is transport state, so this is invalid_request, not a DPoP error.
invalid_grant400A public (SPA) client's refresh token is bound to the key or certificate it was issued with, and another was presented.

client_credentials (M2M):

CodeHTTPMeaning
invalid_client401Missing client_id, unknown app, wrong secret, foreign tenant, or disabled app — all one opaque failure (Unknown client_id or bad client_secret).
unauthorized_client400The application is not opted in to this grant (allow_client_credentials is off — the default). Raised after client authentication, so the flag leaks nothing to an unauthenticated caller.
invalid_target400The requested audience/resource isn't the tenant's Management API identifier.

refresh_token:

CodeHTTPMeaning
invalid_grant400The refresh token is missing, unknown, revoked, expired, or a reuse was detected (which revokes the entire token family).
invalid_scope400The scope re-check on refresh failed.

CIBA (urn:openid:params:grant-type:ciba):

CodeHTTPMeaning
login_required400max_age was exceeded and reauthentication is required — the bound came from max_age at /bc-authorize and the approval is older. This grant only. The code flow decides max_age at /authorize (where the same code is the prompt=none refusal), never at the exchange, so do not wire a retry for it on the authorization_code table above.

The polling codes (authorization_pending, slow_down, expired_token, access_denied) are under CIBA token polling below.

Authorize endpoint (GET or POST /v1/idp/oidc/authorize)

Before a redirect_uri is validated there is nobody to redirect to, so the endpoint answers the branded HTML refusal page described above — HTTP 400, no Location and no JSON body (the page itself names invalid_request). An unknown client_id, a client belonging to another tenant, and a redirect_uri that is not on the application's allow-list all produce the byte-identical page, so a caller cannot tell them apart or enumerate which client_ids exist. The codes below are the failures the endpoint reaches once the redirect_uri is known to be registered:

CodeHTTPMeaning
unsupported_response_type400A response_type the application is not registered for (applications are registered for code unless the implicit or hybrid types were enabled), or one SenseCrypt does not offer (a bare token).
invalid_request400Missing response_type (direct flow); an implicit request without nonce; response_mode=query with an implicit type, or a response_mode SenseCrypt does not offer. A missing redirect_uri is not here: with nothing registered to redirect to, it answers the branded 400 page above.
invalid_target400The client isn't authorized to request the resource audience.
invalid_request_uri400The PAR request_uri is malformed, empty, or already consumed/expired (the last is opaque).
invalid_scope400A requested scope isn't bound to the client.
invalid_request400PKCE required but no code_challenge, or an unsupported code_challenge_method (only S256 is accepted; plain is rejected).
request_not_supported400A request parameter (a JAR request object). This endpoint takes none — push signed request objects to /par. The object's contents are never read, not even to find a redirect target.
request_uri_not_supported400A request_uri that is not one SenseCrypt minted. Only the PAR URN form (urn:ietf:params:oauth:request_uri:…) is accepted.
invalid_request400The application is on the FAPI 2.0/CIBA profile or registered with require_signed_request_object, and this request was not pushed.
login_required400prompt=none, and the request cannot be answered from the browser session: there is no live session, the last ceremony is older than max_age (or the application's default_max_age), the user is no longer allowed into the application, the id_token_hint names another user or was issued to another application, or the request sets enforce_login_hint and the session belongs to someone other than the login_hint email. See Silent authorization.
invalid_request400prompt=none combined with another prompt value; a max_age that is not a non-negative whole number of seconds; or an id_token_hint that does not verify as an id_token this tenant issued. An access token or a logout token sent as the hint is refused here too, although it verifies against the same JWKS.
access_denied400The face ceremony did not end in an authorization: it failed, or it ran out of time (then error_description is session_expired). For a response type that returns tokens from this endpoint, it is also the answer when the user is no longer allowed into the application at the end of the ceremony.

How these reach you. An authorization-endpoint error is delivered to a redirect_uri registered for your application — the one you pushed to /par, or the plain redirect_uri parameter on a direct request — as a redirect carrying error and error_description, so the status above is the code the redirect names, not a status your client sees. When no registered return address can be established from the request, there is no redirect at all: SenseCrypt answers its branded refusal page, which is always HTTP 400. Every error redirect also carries the RFC 9207 iss parameter — and if the request asked for a JARM response mode (jwt, query.jwt, fragment.jwt, form_post.jwt), the error arrives as a signed response JWT with error / error_description / state as its claims (and iss inside the JWT), not as plain query parameters. Parse the JWT before looking for an error.

PAR endpoint (POST /v1/idp/oidc/par)

Success is 201. Errors:

CodeHTTPMeaning
unsupported_response_type400A response_type the application is not registered for, or one SenseCrypt does not offer — the same rule as on /authorize (PAR no longer refuses every non-code type outright).
invalid_request400Unknown client for this issuer, a redirect_uri not on the allow-list, an implicit request without nonce, or a response_mode that is unsupported / query with an implicit type.
invalid_target400The client isn't authorized to request the resource audience.
invalid_scope400A requested scope isn't bound to the client.
invalid_request400PKCE gate (as on /authorize; not applied to an implicit push — there is no code to protect).
invalid_client401Client authentication failed (see Client authentication).
invalid_request400A request_uri was included — RFC 9126 §2.1 forbids it in a pushed request; this endpoint is what mints one.
invalid_request400The client is registered with require_signed_request_object and the push carried no request.
invalid_request400A dpop_jkt parameter and a DPoP proof were both present and named different keys.
invalid_request_object400The signed request object did not verify — a disallowed alg or typ, no usable registered key, a bad signature, an aud that isn't the issuer, a missing/out-of-window nbf or exp, a nested request/request_uri, or an iss/client_id that isn't the authenticated client. One fixed description for every cause — the reason is logged, not returned.

Introspection (POST /v1/idp/oidc/introspect)

Per RFC 7662, an unverifiable, expired, revoked, wrong-audience, or unknown token returns {"active": false} with HTTP 200 — never an error. True errors:

CodeHTTPMeaning
invalid_client401Client authentication failed.
invalid_client401Introspection requires a confidential client — a public/SPA client is rejected.
invalid_request400The token parameter is missing.

A DPoP proof on the introspection request is optional. One that is sent is checked, and a bad one is invalid_dpop_proof or use_dpop_nonce (see DPoP errors). An active response for a bound token carries its cnf, and token_type: "DPoP" for a DPoP-bound one; an active refresh token reads token_type: "refresh_token".

Revocation (POST /v1/idp/oidc/revoke)

Per RFC 7009, an invalid/unknown/already-revoked token returns an empty 200 (no validity oracle). But client authentication still gates the call:

CodeHTTPMeaning
invalid_client401Client authentication failed (cross-tenant client).
invalid_request400The token parameter is missing.

UserInfo (GET or POST /v1/idp/oidc/userinfo)

Failures use the OAuth/OIDC envelope ({"error": …, "error_description": …}), not the management one. A failure about the access token — none presented, or one that does not verify — is 401 with an RFC 6750 WWW-Authenticate challenge:

RequestWWW-Authenticate
A token presented with the Bearer scheme (or as the access_token form parameter)Bearer realm="userinfo", error="invalid_token", error_description="…"
A token presented with the DPoP scheme, or a DPoP-bound token presented as a bearer token (RFC 9449 §7.2)DPoP algs="ES256 PS256", error="…" — the code is invalid_token, invalid_dpop_proof, or use_dpop_nonce
No token at allboth challenges, neither carrying an error (RFC 6750 §3.1): Bearer realm="userinfo", DPoP algs="ES256 PS256" — the body's error is invalid_request

invalid_token covers: a bad/unverifiable access token, a token whose token_use isn't access, an expired or revoked token, a client-credentials token (there is no End-User behind it), and a sub that resolves to no active user. A use_dpop_nonce response also carries a DPoP-Nonce header to retry with. Branch on the body's error and the challenge scheme — never on error_description.

Two failures land before the token is read, and they are 400 invalid_request with no WWW-Authenticate header: an Authorization header whose scheme is neither Bearer nor DPoP (or one of those two schemes carrying an empty token), and a request that presents the token by both methods at once — the header and the form parameter, where RFC 6750 §2 allows exactly one. The endpoint is rate-limited per client IP as well, so a flood answers 429 — the one failure here that uses the management envelope.

Protected resource (GET or POST /v1/idp/oidc/resource)

The protected resource endpoint fails the way UserInfo does, with the same envelope and the same 401 challenges. The Bearer challenge names realm="userinfo" here too. The differences:

  • The token travels only in the Authorization header, so there is no form parameter and no "both methods at once" refusal. A header with another scheme, or with an empty token, is 400 invalid_request with no challenge.
  • A client-credentials token from a registered application is accepted, not refused.
  • invalid_token covers a token that does not verify, has expired or been revoked, is not an access token, was issued to an application that is no longer registered, or names a user who is no longer allowed into that application. As at UserInfo, it also covers a token presented without the key it is bound to: a DPoP-bound token sent as a bearer token, a proof signed by another key, or a certificate-bound token without that certificate.
  • A missing DPoP proof, or one that fails a check, is invalid_dpop_proof (401). The proof failed, not the token.
  • A DPoP proof from a FAPI 2.0/CIBA application without a server nonce is use_dpop_nonce (401, with DPoP-Nonce).
  • A flood answers 429 in the management envelope. This endpoint has its own per-IP budget.

RP-Initiated Logout (GET or POST /v1/idp/oidc/logout)

An id_token_hint is not required: without one the endpoint shows a confirmation page (HTTP 200) and, once confirmed, signs the user out on its own signed-out page. Failures are 400 HTML pages with error: "invalid_request", and nothing is signed out. Triggers: a hint that does not verify as one this issuer signed; a hint that is not an id_token — an access token or a logout token is refused even though it verifies against the same JWKS; a client_id that does not match the hint's aud; a hint whose application is no longer registered; a post_logout_redirect_uri with neither an id_token_hint nor a client_id to validate it against; or a client_id / post_logout_redirect_uri pair that is unknown or not on the client's dedicated post-logout allow-list (both answer with one identical page, so a client_id cannot be enumerated).

CIBA backchannel start (POST /v1/idp/oidc/bc-authorize)

CodeHTTPMeaning
invalid_client401Unknown client for this issuer.
unauthorized_client400The client isn't registered for CIBA.
invalid_request400scope must include openid; exactly one hint is required; a client_notification_token is required for ping/push delivery and, when supplied, must be a bearer token (RFC 6750 b64token — letters, digits, -._~+/, trailing =; no spaces or line breaks) of at most 1024 characters; or a signed authentication request (request) does not verify, is unsigned where one is required, or is presented a second time (its jti is single-use).
invalid_binding_message400binding_message is longer than 140 displayable characters (counted after Unicode NFC normalisation) or contains characters that cannot be displayed as plain text: control characters, line/paragraph separators, or bidirectional formatting controls. Unassigned code points (a newer emoji, say) are accepted.
invalid_target400The client isn't authorized to request the resource audience.
unknown_user_id400The hint didn't resolve to an eligible user. Deliberately the same whether the user is unknown or merely ungated, or the hint is an ID token issued to another application.
invalid_scope400A requested scope isn't bound to the client.
invalid_request400A face-PKI ceremony whose parameters are wrong: an unknown face_pki_kind, a missing or over-length (255 bytes) face_pki_context_b64, a face_pki_payload_hash_b64 that is not 32 or 64 bytes or is sent on a ceremony other than sign, a face_pki_kem_ciphertext_b64 whose length is not the tenant's ML-KEM ciphertext size or is sent on a ceremony other than decapsulate, bad base64 in any of them, or a ceremony without a binding_message or without a client_notification_token (required in every delivery mode). Also a face_pki_scope other than app or tenant, and face_pki_scope=tenant from an application without face_pki_tenant_scope_allowed. The description names the parameter.
unauthorized_client400A face-PKI ceremony from an application whose tenant has no face PKI, or that has no face_pki_delivery_endpoint, or no CIBA delivery mode.

invalid_binding_message is the CIBA Core §13 code — an over-length or non-displayable binding_message is not reported as invalid_request. Earlier releases capped the message at 512 characters and used invalid_request; see the changelog.

CIBA callback contract (ping and push)

For ping and push clients SenseCrypt POSTs to your registered notification endpoint with Authorization: Bearer <client_notification_token>. How your endpoint answers decides what happens next:

Your endpoint answersResult
2xxDelivered. A ping client then collects tokens at the token endpoint; a push client has its tokens (and the auth_req_id is spent).
3xxFinal, never retried, never followed. A redirect could carry the bearer to another host, so it is treated as a refusal.
4xxFinal, never retried (CIBA §10.2). For a push client the approved authorization is lost — the tokens had nowhere to go and push clients may not poll; start a new request. A ping client can still collect at the token endpoint.
5xx or a transport failureRetried, up to 5 attempts in total; after the fifth failure the notification is abandoned as above.

Failed requests are notified too: a ping client receives the same { "auth_req_id" } body and learns access_denied (or expired_token) at the token endpoint; a push client receives { "auth_req_id", "error": "access_denied" | "expired_token", "error_description" }. An approval that lands in the last seconds of the request window is still delivered (for a ping client the token endpoint then answers expired_token).

CIBA token polling (at POST /v1/idp/oidc/token)

These are returned in the RFC 6749 shape at HTTP 400. The first two are normal poll responses, not terminal failures:

CodeHTTPMeaning
authorization_pending400The user hasn't approved yet. Keep polling at the advertised interval.
slow_down400You're polling faster than the advertised interval. Back off.
expired_token400The auth_req_id has expired.
access_denied400The user denied the request. On a face-PKI ceremony: the user cancelled, or the face did not match. On a tenant-scope ceremony, it can also mean that an admin turned off face_pki_tenant_scope_allowed on the application while the ceremony was pending. In that case the phone also gets access_denied. The Activity API shows fail_reason scope_withdrawn.
unauthorized_client400A push-delivery client must not poll the token endpoint.
invalid_grant400Unknown or already-used auth_req_id.

A face-PKI ceremony ends with its own codes, returned here and in a push client's error payload under their own names — they are steps of the application's request, not facts about the user. A sign-in never returns them.

CodeHTTPMeaning
delivery_failed400The application's delivery endpoint refused the result or could not be reached, and the user gave up.
key_unavailable400After the face match, the user's face token could not provide the key the ceremony needs — a key class the token cannot provide, an algorithm the app cannot use, or a ciphertext that does not fit the user's key. The phone never says which.
invalid_request400The phone refused the ceremony it was handed: malformed data, a context over 255 bytes, a hash of the wrong size, or a delivery endpoint that is not https.
stale_token400Another session updated the user's face token while the ceremony ran. No receipt was minted. Start a new ceremony.

Client authentication errors

Across /token, /par, /introspect, /revoke, and /bc-authorize, all client-authentication failures collapse to invalid_client (401) so the endpoint can't leak which side failed. This covers: unknown client_id, wrong secret, a malformed Authorization header, multiple auth mechanisms, an auth method the client isn't registered for, a public/SPA client presenting a credential, any client_assertion verification failure (client_secret_jwt / private_key_jwt), and every mutual TLS failure (tls_client_auth / self_signed_tls_client_auth): no certificate on the request, a chain that fails path validation against the registered anchors, a subject DN that doesn't match, a presented certificate that isn't the registered one, or one outside its validity window. A mutual TLS request must also carry client_id in the body — the certificate is the credential, but it is not how the client is located.

DPoP errors

DPoP failures have two shapes depending on which side of the issuer you are talking to. Every refusal carries one fixed description (The DPoP proof is invalid); the actual reason is logged, not returned.

CodeHTTPWhereMeaning
invalid_dpop_proof400/token, /par, /introspectThe proof failed a check: more than one DPoP header, a typ that isn't dpop+jwt, an alg outside dpop_signing_alg_values_supported, a missing or private-key-bearing jwk header, a bad signature, a missing jti/htm/htu/iat, an htm/htu that doesn't match the request, a stale or future iat, or a replayed jti.
invalid_dpop_proof401/userinfo, /resourceThe same checks at a protected resource, plus an ath that doesn't match the presented access token, or no proof at all for a DPoP-bound token. Carries WWW-Authenticate: DPoP algs="ES256 PS256", error="invalid_dpop_proof", error_description="The DPoP proof is invalid".
use_dpop_nonce400/tokenA server nonce is required — every FAPI client's proof must carry one — or the one supplied is stale or not ours. The response carries a DPoP-Nonce header; retry with that value in the proof's nonce claim.
use_dpop_nonce400/par, /introspectA nonce is never required at these endpoints, but one that is present must be ours and fresh. Same DPoP-Nonce retry.
use_dpop_nonce401/userinfo, /resourceAs at /token (required for a FAPI client), with both WWW-Authenticate: DPoP … and DPoP-Nonce.
invalid_token401/userinfo, /resourceA DPoP-bound token presented with the Bearer scheme, an unbound token presented with the DPoP scheme, a proof from a key other than the token's cnf.jkt, a certificate-bound token whose certificate was not presented, or a cnf shape SenseCrypt cannot verify. Carries the WWW-Authenticate challenge for the scheme in play.

Treat use_dpop_nonce as a retry, not a failure: read the DPoP-Nonce header, put it in the next proof, and repeat the request once.


SAML errors

SAML protocol endpoints use the OAuth/OIDC envelope shape. Common codes:

CodeHTTPMeaning
unauthorized401Unknown SP entityID, IdP-initiated SSO not allowed for the SP, or a failed delivery gate — opaque, to prevent entityID enumeration.
invalid_request400Invalid SAMLRequest (parse failure), request-signature verification failure, an unregistered AssertionConsumerServiceURL, an unsatisfiable NameID policy (carrying the SAML status URN urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy), or neither SAMLRequest nor sp present.
invalid_grant400The post-back session has no authorized subject.
not_found404The SAML session isn't ready or already delivered, or ?kid= names no published certificate.

Single Logout (SLO) is not implemented — there is no SLO endpoint or LogoutRequest handling, and SAML sign-ins neither establish nor read the OP browser session (sc_op_session is OIDC-only), so there is no IdP-side SAML session to propagate a logout from. Request-signature verification and assertion encryption require RSA SP keys.


SCIM errors

SCIM uses the envelope shown above. The scimType field is present on 400-class errors; auth failures (401/403) carry only a detail message.

scimTypeHTTPWhen
invalidFilter400An unknown attribute path, unsupported operator, or malformed/over-long filter expression. Never a silent full-directory listing.
invalidValue400A missing required field on create (userName + a primary email), an illegal remove (of active, userName, or emails), leaving a user with no primary email, attributes and excludedAttributes supplied together, a bad date in a filter, or a malformed bulk request.
invalidSyntax400An unsupported PATCH op (not add/replace/remove).
invalidPath400An unsupported PATCH value-path, or a bulk path/id problem.
noTarget400A PATCH value-path remove/replace matched no value.
uniqueness409A userName/email collision (users) or a displayName collision (groups).
tooMany413More than 1000 operations in a bulk request.
tooLarge413A bulk payload over 1 MiB. (Note: tooLarge is a SenseCrypt extension, not a standard RFC 7644 scimType.)

Errors without a scimType:

HTTPWhen
401Missing or invalid bearer token — detail names the reason (e.g. Admin access token required).
403The token lacks the scim:read / scim:write capability for the method — detail is missing capability: scim:read (or scim:write).
404User/Group/Schema not found. (A console-managed group is a deliberate 404 to a SCIM caller — no leak.)
412A conditional write whose If-Match ETag doesn't match the current version (resource has changed).

A remove on members without a target is rejected as 400 invalidValue — it is never treated as a clear-all.

Supported filter operators: eq ne co sw ew gt ge lt le pr, plus and/or/not, parenthesized grouping, and emails[...] value paths. Multi-valued complex attributes support presence (pr) only.


Device-plane errors

Every phone-originated request is a signed request. Device errors use the management envelope; the mobile SDK branches on error and, for unauthorized, on the kind field. All of these are 401.

errorkindMeaning
signature_required—One or more of the required signed-request headers (X-Device-Id, X-Device-Key-Id, X-Timestamp, X-Signature) is missing. Also returned by /register/complete and /signup/complete when a new account tries to join a phone that signs its enrolments (protocol 11) without that signature; the registration token stays usable.
unauthorizedbad_signed_headersA device/key id isn't a valid UUID, the timestamp isn't parseable, or the signature isn't valid base64.
unauthorizedclock_skewThe X-Timestamp is outside the allowed skew window. Recoverable — retry after an NTP sync.
unauthorizeddevice_key_revokedThe device key is unknown/revoked, or the user is suspended or deleted. Terminal for the SDK — wipe and re-enroll. When the key was revoked because the user added the app on another phone under a tenant that allows one device per user, the body also carries cause: "device_replaced" (protocol 12), and the SDK reports deviceKeyReplaced instead: the account moved, so do not offer to add it here again.
unauthorizedunsupported_key_algThe key's registered algorithm isn't verifiable by this build.
unauthorizedbad_signatureThe signature doesn't verify (against the current key or, during a rotation overlap, the previous key).
unauthorizedreplayed_requestThis exact signed request was already used — a verbatim replay.

Protocol version floor

CodeHTTPMeaning
upgrade_required426The X-Protocol-Version header is absent, non-integer, or below the supported floor. The body carries min_supported and current; the SDK maps this to a force-update state.

The floor is protocol 10. In-app sign-in applies the same floor at its own endpoint. An older app gets 426 there in the OAuth/OIDC envelope. That body has error and error_description, and no min_supported or current.

Device key rotation (/v1/device/keys/rotate/*)

These are 400 with a specific error code: no_keys_to_rotate, bad_nonce, no_rotation_in_progress, rotation_nonce_expired, rotation_nonce_mismatch, rotation_set_mismatch, bad_new_key_bytes, and bad_new_key_signature. Each message is self-describing.

One refusal is a 403: {"error": "forbidden", "code": "rotation_row_membership"}. The signing key joined this phone's device record without proof that it belongs to the phone, and other accounts hold keys there, so it may not rotate them. Nothing is changed or wiped. A second account added on a phone running an older app is in this state until the app's next background sync (on app open or on opening Settings) confirms it; retry after that.

PIN and registration

CodeHTTPMeaning
pin_invalid401Wrong PIN. The body carries attempts_left.
device_replacement_required409The tenant allows one device per user, this /register/complete or /signup/complete would sign the user's other phone(s) out, and the body did not carry replace_other_devices: true. The body lists those phones as replaces_devices (name, OS and last use, never an id). Nothing was written and the registration token is still usable: show the confirmation and send the same completion again with the flag. The Mobile SDK (protocol 12) does this itself. An app below protocol 12 cannot confirm, so it is answered 426 upgrade_required (with min_supported: 12) instead and shows its update sheet.
pin_locked403PIN attempts exhausted, or the code is already locked.
pin_expired410No active PIN, or the PIN's TTL has expired.
cooldown_active429A PIN-resend cooldown is in effect. Carries a Retry-After header.
token_invalid401A registration/signup token is unknown, consumed, or unverified.
token_expired410A registration/signup token is past its TTL.
email_exists409Self-signup on an already-enrolled email.
account_suspended403The address belongs to a suspended user. Answered to the correct PIN at /register/verify, which it consumes, and to /ciba/signup-start; never at /register/start, which answers a suspended address as an active one. Reinstating the user lifts it.

App attestation

When the tenant's attestation mode is enforce (the deployment default applies to tenants on the stock Authenticator; a tenant that ships its own app chooses its mode), every attestation failure returns 403 with the machine error field forbidden and the message attestation_failed. In audit mode the same failure is logged and the request proceeds. The specific subreason (nonce mismatch, counter replay, integrity verdict, and so on) is intended for server logs; treat the client-visible failure as opaque and branch only on error: "forbidden" / message: "attestation_failed".


Management API validation errors

Every request body on the Management API (/v1/admin/*) is validated the same way, and every field-level refusal — a malformed value, a missing required field, an unknown or immutable field, or a registration rule such as the FAPI 2.0/CIBA rules on /v1/admin/oidc-apps — is answered with 422 in the management envelope plus a details[] array. The shape is the same on create (POST) and on update (PATCH/PUT): the same rule produces the same code on the same field whichever way it is reached.

{
  "error": "validation_error",
  "message": "Request validation failed",
  "details": [
    {
      "loc": ["body", "jwks"],
      "field": "jwks",
      "msg": "A FAPI application with CIBA signs every backchannel request; register its request-signing keys (jwks or jwks_uri) so they can be verified",
      "type": "fapi_ciba.requires_signing_keys",
      "code": "fapi_ciba.requires_signing_keys"
    }
  ]
}
  • error is always validation_error and message is always Request validation failed — branch on details[], never on the top-level message.
  • details[].field is the request-body field the entry is about. For a rule that spans several fields it is the first offending one (the loc is rewritten to ["body", <field>] too, so a client that only knows loc places it the same way).
  • details[].code is the stable machine code for the rule — either SenseCrypt's own (fapi.requires_pkce, fapi_ciba.requires_signing_keys, jwks.private_material, immutable_field, …) or the validator's generic one (missing, string_too_long, extra_forbidden, …). type repeats it. Branch on code; msg is the server's fallback wording and may change.
  • Secrets never round-trip: the submitted value is not echoed back in any entry.

Three codes apply to every body, not to one field's rule:

codeMeaning
extra_forbiddenThe body names a field this route does not have. Unknown fields are refused, not ignored — a typo'd key or a field that belongs to another route is a 422 naming it, never a silent 200 with nothing changed.
immutable_fieldThe field exists but is fixed when the resource is created — client_type, token_endpoint_auth_method or billing_model on an application, slug or fapi_profile on a tenant. Create a new resource to use another value; the request is refused before anything is read or written.
field_movedThe field exists but is set on another route now; msg names where. Today: op_session_ttl_seconds on PATCH /v1/admin/tenants/{id} — it moved to PATCH /v1/admin/issuer (see the changelog).

Application registration rule codes

These are the details[].code values the OIDC-application routes (POST / PATCH /v1/admin/oidc-apps) raise for the protocol rules — the security profile, client keys, mutual TLS, signed requests, CIBA and back-channel logout. Grouped by prefix; the same code is raised on create and on update.

fapi.* — the FAPI 2.0/CIBA profile

codeRefused because
fapi.requires_confidential_clientclient_type is not confidential.
fapi.requires_pkcerequire_pkce is not true.
fapi.requires_key_or_certificate_authtoken_endpoint_auth_method is not private_key_jwt, tls_client_auth or self_signed_tls_client_auth.
fapi.requires_sender_constrained_tokensNeither dpop_bound_access_tokens nor tls_client_certificate_bound_access_tokens is on.
fapi.forbids_rs256id_token_signed_response_alg is RS256.
fapi.requires_code_onlyresponse_types is not exactly ["code"].
fapi.requires_https_redirect_urisAn allowed_redirect_uris entry is http and not a loopback address. msg names the offending URI.
fapi.forbids_ciba_pushbackchannel_token_delivery_mode is push.
fapi.inherited_from_tenantThe tenant is a FAPI 2.0/CIBA tenant; its applications cannot set another profile or clear it.

first_party.* — in-app sign-in (first_party_challenge_enabled on /v1/admin/oidc-apps)

codeRefused because
first_party.requires_dpopfirst_party_challenge_enabled is true but dpop_bound_access_tokens is off.
first_party.no_certificate_bindingfirst_party_challenge_enabled is true while tls_client_certificate_bound_access_tokens is on.
first_party.requires_custom_appfirst_party_challenge_enabled is true on a tenant whose mobile app is the stock Authenticator (the Mobile App page's source is not the tenant's own app).

face_pki.* — face PKI (on /v1/admin/oidc-apps, and the first two on POST /v1/admin/tenants)

codeRefused because
face_pki.setting_requiredface_pki_enabled is true but face_pki_kem_algorithm or face_pki_dsa_algorithm is missing.
face_pki.requires_enabledA parameter set was sent with face_pki_enabled false.
face_pki.requires_cibaface_pki_delivery_endpoint is set but the application has no backchannel_token_delivery_mode — including a PATCH that clears the mode while an endpoint stands.
face_pki.tenant_disabledface_pki_delivery_endpoint is set on an application whose tenant has no face PKI.
face_pki.tenant_scope_requires_face_pkiface_pki_tenant_scope_allowed is true on an application whose tenant has no face PKI, on create or PATCH.

A face-PKI tenant on a deployment whose licence has no Face PKI is refused at creation with 400 face_pki_unavailable, not a 422; the three tenant fields are immutable_field on every update.

fapi_ciba.* — a FAPI application with a CIBA delivery mode

codeRefused because
fapi_ciba.requires_signing_keysNo jwks / jwks_uri on file, so its signed backchannel requests could not be verified.
fapi_ciba.requires_certificate_bindingtls_client_certificate_bound_access_tokens is off and the application has not accepted DPoP-bound CIBA tokens (ciba_dpop_binding_allowed with dpop_bound_access_tokens on).

jar.* — signed request objects

codeRefused because
jar.requires_keysrequire_signed_request_object is on with no jwks / jwks_uri to verify against.

jwks.* — registered client keys

codeRefused because
jwks.requires_keyed_methodjwks / jwks_uri were sent for a method that registers none — only private_key_jwt and the two mutual TLS methods may carry them.
jwks.exactly_one_sourceA private_key_jwt application must register exactly one of jwks / jwks_uri.
jwks.one_sourceA mutual TLS application may register one of them, or neither — not both.
jwks.unreachableThe jwks_uri could not be fetched, or did not return a valid JWK Set, when it was saved. One constant sentence; the cause is logged.
jwks.invalidThe inline jwks is not a valid JWK Set.
jwks.too_largeLarger than 32 KB.
jwks.too_many_keysMore than 16 keys.
jwks.kid_requiredA key has no kid.
jwks.duplicate_kidTwo keys share a kid.
jwks.private_materialA key carries private members (d, p, q, dp, dq, qi, oth, k). Private material is never stored.
jwks.alg_mismatchA key's alg isn't allowed for its kty — ES256/ES384 for EC, PS256/RS256 for RSA.
jwks.key_revokedA key in the set was revoked. A revoked key can never be registered again, whether inline or published at the jwks_uri the application switches to. The mutual TLS fields answer anchors.key_revoked, subject_dn.key_revoked and cert.key_revoked the same way.

mtls.* — mutual TLS

codeRefused because
mtls.listener_unavailableThis deployment runs no mutual TLS listener (its discovery publishes no mtls_endpoint_aliases), so neither mutual TLS client authentication nor tls_client_certificate_bound_access_tokens can be registered.
mtls.subject_dn_requiredtls_client_auth without tls_client_auth_subject_dn.
mtls.trust_anchors_requiredtls_client_auth without tls_client_auth_ca_pem.
mtls.subject_dn_invalidtls_client_auth_subject_dn is not parseable as an RFC 4514 distinguished name.
mtls.certificate_requiredself_signed_tls_client_auth without tls_client_certificate_pem.
mtls.certificate_not_applicabletls_client_certificate_pem sent for tls_client_auth.
mtls.pki_fields_not_applicabletls_client_auth_subject_dn / tls_client_auth_ca_pem sent for self_signed_tls_client_auth.
mtls.fields_require_mtls_methodAny mutual TLS registration field sent for a method that is not a mutual TLS one.

ciba.* — CIBA registration

codeRefused because
ciba.requires_client_authCIBA is not available to a spa (public) client.
ciba.dpop_binding_requires_dpopciba_dpop_binding_allowed is true but dpop_bound_access_tokens is off — accepting DPoP-bound CIBA tokens needs DPoP-bound tokens. Also raised by a PATCH that turns dpop_bound_access_tokens off on an application still carrying the acceptance (the merged state is checked): send ciba_dpop_binding_allowed: false in the same request. The console does this for you.
ciba.endpoint_requiredping/push delivery without backchannel_client_notification_endpoint.
ciba.user_code_unsupportedbackchannel_user_code_parameter was opted into. This issuer is passwordless and has no per-user code to verify, so the opt-in is refused rather than silently ignored.

backchannel_logout_initiators.* — back-channel logout

codeRefused because
backchannel_logout_initiators.invalidA value outside the vocabulary (rp-logout, session-revoked, access-revoked, account-deleted, email-identifier-changed, session-expired).
backchannel_logout_initiators.requires_rp_logoutThe list does not contain rp-logout, which is always on. Refused rather than silently repaired.

response_types.*, redirect_uris.*, url.* — request shape

codeRefused because
response_types.invalidA response_types entry this issuer does not offer (a bare token, an unknown word).
redirect_uris.requiredallowed_redirect_uris is empty — at least one is required.
redirect_uris.invalidA allowed_redirect_uris or post_logout_redirect_uris entry is not a usable absolute URL. msg names the entry and the reason.
url.https_requiredA URL-shaped field is not an absolute https URL — jwks_uri, backchannel_client_notification_endpoint, backchannel_logout_uri, terms_url, privacy_url.
url.invalidterms_url / privacy_url is not a usable absolute URL.
url.private_hostA URL-shaped field's host is private, loopback, link-local or otherwise reserved. SenseCrypt calls these hosts server-side, so an internal target is never stored.

Client credential rotation

The credential rotation routes (…/keys, …/trust-anchors, …/subject-dns, …/certificates under /v1/admin/oidc-apps/{client_id} and /v1/admin/m2m-apps/{id}) answer these states with 409 in the management envelope:

errorMeaning
last_current_keyThe action would leave an application that authenticates with this credential with none current. Rotate a new one in first.
key_state_conflictThe item is not in a state this action applies to, e.g. reactivating a current key or revoking a revoked one.
reactivation_expiredThe item was retired more than 30 days ago and can no longer be reactivated.
keys_not_managed_hereThe application does not authenticate with this kind of credential, or its keys live at its jwks_uri (rotate them there).
no_client_secretPOST /v1/admin/m2m-apps/{id}/secret on an app that signs in with a key or a certificate: it has no secret to rotate.

Field refusals on the same routes are 422 entries, prefixed by kind (jwks, anchors, subject_dn, cert):

codeRefused because
*.key_existsThis key or certificate is already registered.
*.key_revokedIt was revoked and can never be registered again.
*.duplicate_kidAn accepted key already uses this kid.
*.too_many_keysThe application already accepts the most it may at a time.
*.rotate_insteadOnly one can be current (subject DN, self-signed certificate): rotate to replace it.
jwks.single_key_required / anchors.single_requiredSend exactly one key or one CA certificate per call.

M2M apps in a FAPI 2.0 tenant (POST /v1/admin/m2m-apps and its …/keys routes) add:

codeRefused because
auth_method.fapi_tenant_onlyprivate_key_jwt, tls_client_auth and self_signed_tls_client_auth are offered in FAPI 2.0 tenants only.
jwks.required, tls_client_auth_ca_pem.required, tls_client_auth_subject_dn.required, tls_client_certificate_pem.requiredThe chosen auth_method needs this credential to create the app.
jwks.not_allowed (and the same for the other credential fields)The field is not used by the chosen auth_method.
tls_client_certificate_bound_access_tokens.fapi_onlyCertificate-bound tokens are for FAPI methods; a secret app mints plain bearer tokens.
jwks.fapi_algThe key does not name alg ES256 (EC) or PS256 (RSA), or names one its key type can't use.
jwks.fapi_kty / jwks.fapi_curve / jwks.key_too_smallNot an EC or RSA key; an EC key not on P-256; an RSA key under 2048 bits.

A FAPI M2M app's bound token presented at the Management API without its proof or certificate is a 401 in the management envelope with kind: "sender_constraint" and a WWW-Authenticate challenge. See Machine-to-machine → Troubleshooting.

Other prefixes on the same routes follow the same shape and carry a self-describing msg: spa.* (what a public client may not change — PKCE, refresh rotation, certificate binding, credentials, keys), refresh.* (rotation needs a bounded family lifetime and a non-zero overlap), signup.* (allow_signup and signup_group_id travel together), cert.* and anchors.* (a pasted client certificate or trust-anchor bundle that is too large, unparseable, expired, not a CA, on too small a key, or not usable for TLS client authentication), metadata.* (client_metadata key, depth and size caps), list.* and string_too_long (list and string bounds), and field.replaced (a field superseded by another — msg names it).

Statuses that are not field refusals keep their own codes and shapes: a clash with an existing resource is 409 (slug_taken, tenant_name_exists), a missing capability 403, an unknown id in another tenant 404, and a billing block 402 payment_required. Earlier releases answered many of the application registration rules as 400 bad_request with a prose message; those are now 422 entries with a code — see the changelog.

Tenant mobile app, custom domains and sign-in host

PATCH /v1/admin/mobile-app answers these with 409 in the management envelope:

errorMeaning
mobile_app_switch_confirmation_requiredapp_source changed without confirm_app_switch: true. Switching the app a tenant's users run takes effect immediately.
mobile_app_project_change_confirmation_requiredThe uploaded Google service-account key belongs to a different Google Cloud project than the stored one; resend with confirm_project_change: true.
mobile_app_enforce_confirmation_requiredThe change would make enforce the active attestation mode, or would change the app identity while enforce is active, while live device keys are failing or have never been checked for that identity; the body carries the counts as attestation_readiness. Resend with confirm_enforce: true. Those keys are refused at their next sign-in.

Its one route-level 422 rule is app_link_host.not_tenant_host: the app-link host must be the tenant's slug host or one of its verified custom domains. Field rules (app name, package name, fingerprints, team ID, bundle ID, URL scheme, store URLs, the app-link hostname's shape, the key JSON) use the validation envelope above.

The custom-domain and sign-in-host routes answer:

StatuserrorMeaning
400bad_requestThe hostname is not a valid public FQDN, is an IP or private host, or is one this deployment already serves.
402resource_requires_paymentThe account is at its included domains, pending ones counted, and has no saved card.
409domain_existsThis tenant already holds that domain.
409domain_limitThe account holds the most domains this deployment allows.
409domain_is_signin_hostThe domain is the tenant's passkey sign-in host. Change the sign-in host first, then delete the domain.
409signin_host_confirmation_requiredThe sign-in host change would retire live passkeys and confirm_reenrolment: true was not sent.
409signin_host_spans_registrable_domainsPUT /v1/admin/signin-host was asked to set a host while the tenant's verified domains span more than one registrable domain, so no single passkey host can cover them. Resetting to the default is still allowed; otherwise split the tenant.
422signin_host_invalidThe requested sign-in host is not one of the tenant's choices.

A hostname another tenant has already verified is reported at verification, not at creation, as domain_taken.


Rate limiting (all surfaces)

Exceeding a rate limit returns 429 with error: "rate_limited", a Retry-After header, and a retry_after_seconds field in the body. See Rate limits for the full policy.


On this page