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_clientis401; every other OAuth error (invalid_grant,invalid_request,invalid_target,invalid_scope,unsupported_grant_type,unsupported_response_type, and the CIBA codes) is400.
OAuth / OIDC errors
Token endpoint (POST /v1/idp/oidc/token)
| Code | HTTP | Meaning |
|---|---|---|
unsupported_grant_type | 400 | grant_type isn't one of authorization_code, refresh_token, client_credentials, or CIBA. |
invalid_grant | 400 | The 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_grant | 400 | PKCE 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_scope | 400 | A requested scope isn't granted to the client (re-checked here and on refresh). |
invalid_grant | 400 | The 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_request | 400 | The 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_grant | 400 | A public (SPA) client's refresh token is bound to the key or certificate it was issued with, and another was presented. |
client_credentials (M2M):
| Code | HTTP | Meaning |
|---|---|---|
invalid_client | 401 | Missing client_id, unknown app, wrong secret, foreign tenant, or disabled app — all one opaque failure (Unknown client_id or bad client_secret). |
unauthorized_client | 400 | The 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_target | 400 | The requested audience/resource isn't the tenant's Management API identifier. |
refresh_token:
| Code | HTTP | Meaning |
|---|---|---|
invalid_grant | 400 | The refresh token is missing, unknown, revoked, expired, or a reuse was detected (which revokes the entire token family). |
invalid_scope | 400 | The scope re-check on refresh failed. |
CIBA (urn:openid:params:grant-type:ciba):
| Code | HTTP | Meaning |
|---|---|---|
login_required | 400 | max_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 /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:
| Code | HTTP | Meaning |
|---|---|---|
unsupported_response_type | 400 | A 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_request | 400 | Missing 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_target | 400 | The client isn't authorized to request the resource audience. |
invalid_request_uri | 400 | The PAR request_uri is malformed, empty, or already consumed/expired (the last is opaque). |
invalid_scope | 400 | A requested scope isn't bound to the client. |
invalid_request | 400 | PKCE required but no code_challenge, or an unsupported code_challenge_method (only S256 is accepted; plain is rejected). |
request_not_supported | 400 | A 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_supported | 400 | A request_uri that is not one SenseCrypt minted. Only the PAR URN form (urn:ietf:params:oauth:request_uri:…) is accepted. |
invalid_request | 400 | The application is on the FAPI 2.0/CIBA profile or registered with require_signed_request_object, and this request was not pushed. |
How these reach you. An authorization-endpoint error is delivered to a
redirect_uriregistered for your application — the one you pushed to/par, or the plainredirect_uriparameter on a direct request — as a redirect carryingerroranderror_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 HTTP400. Every error redirect also carries the RFC 9207issparameter — and if the request asked for a JARM response mode (jwt,query.jwt,fragment.jwt,form_post.jwt), the error arrives as a signedresponseJWT witherror/error_description/stateas its claims (andissinside 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:
| Code | HTTP | Meaning |
|---|---|---|
unsupported_response_type | 400 | A 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_request | 400 | Unknown 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_target | 400 | The client isn't authorized to request the resource audience. |
invalid_scope | 400 | A requested scope isn't bound to the client. |
invalid_request | 400 | PKCE gate (as on /authorize; not applied to an implicit push — there is no code to protect). |
invalid_client | 401 | Client authentication failed (see Client authentication). |
invalid_request | 400 | A request_uri was included — RFC 9126 §2.1 forbids it in a pushed request; this endpoint is what mints one. |
invalid_request | 400 | The client is registered with require_signed_request_object and the push carried no request. |
invalid_request | 400 | A dpop_jkt parameter and a DPoP proof were both present and named different keys. |
invalid_request_object | 400 | The 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:
| Code | HTTP | Meaning |
|---|---|---|
invalid_client | 401 | Client authentication failed. |
invalid_client | 401 | Introspection requires a confidential client — a public/SPA client is rejected. |
invalid_request | 400 | The token parameter is missing. |
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:
| Code | HTTP | Meaning |
|---|---|---|
invalid_client | 401 | Client authentication failed (cross-tenant client). |
invalid_request | 400 | The 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:
| Request | WWW-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 all | both 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.
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)
| Code | HTTP | Meaning |
|---|---|---|
invalid_client | 401 | Unknown client for this issuer. |
unauthorized_client | 400 | The client isn't registered for CIBA. |
invalid_request | 400 | scope 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_message | 400 | binding_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_target | 400 | The client isn't authorized to request the resource audience. |
unknown_user_id | 400 | The hint didn't resolve to an eligible user. Deliberately the same whether the user is unknown or merely ungated. |
invalid_scope | 400 | A requested scope isn't bound to the client. |
invalid_binding_messageis the CIBA Core §13 code — an over-length or non-displayablebinding_messageis not reported asinvalid_request. Earlier releases capped the message at 512 characters and usedinvalid_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 answers | Result |
|---|---|
2xx | Delivered. A ping client then collects tokens at the token endpoint; a push client has its tokens (and the auth_req_id is spent). |
3xx | Final, never retried, never followed. A redirect could carry the bearer to another host, so it is treated as a refusal. |
4xx | Final, 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 failure | Retried, 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:
| Code | HTTP | Meaning |
|---|---|---|
authorization_pending | 400 | The user hasn't approved yet. Keep polling at the advertised interval. |
slow_down | 400 | You're polling faster than the advertised interval. Back off. |
expired_token | 400 | The auth_req_id has expired. |
access_denied | 400 | The user denied the request. |
unauthorized_client | 400 | A push-delivery client must not poll the token endpoint. |
invalid_grant | 400 | Unknown or already-used auth_req_id. |
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.
| Code | HTTP | Where | Meaning |
|---|---|---|---|
invalid_dpop_proof | 400 | /token, /par, /introspect | The 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_proof | 401 | /userinfo | The 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_nonce | 400 | /token | A 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_nonce | 400 | /par, /introspect | A nonce is never required at these endpoints, but one that is present must be ours and fresh. Same DPoP-Nonce retry. |
use_dpop_nonce | 401 | /userinfo | As at /token (required for a FAPI client), with both WWW-Authenticate: DPoP … and DPoP-Nonce. |
invalid_token | 401 | /userinfo | A 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:
| Code | HTTP | Meaning |
|---|---|---|
unauthorized | 401 | Unknown SP entityID, IdP-initiated SSO not allowed for the SP, or a failed delivery gate — opaque, to prevent entityID enumeration. |
invalid_request | 400 | Invalid 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_grant | 400 | The post-back session has no authorized subject. |
not_found | 404 | The 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
LogoutRequesthandling, and no IdP-side SSO 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.
scimType | HTTP | When |
|---|---|---|
invalidFilter | 400 | An unknown attribute path, unsupported operator, or malformed/over-long filter expression. Never a silent full-directory listing. |
invalidValue | 400 | A 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. |
invalidSyntax | 400 | An unsupported PATCH op (not add/replace/remove). |
invalidPath | 400 | An unsupported PATCH value-path, or a bulk path/id problem. |
noTarget | 400 | A PATCH value-path remove/replace matched no value. |
uniqueness | 409 | A userName/email collision (users) or a displayName collision (groups). |
tooMany | 413 | More than 1000 operations in a bulk request. |
tooLarge | 413 | A bulk payload over 1 MiB. (Note: tooLarge is a SenseCrypt extension, not a standard RFC 7644 scimType.) |
Errors without a scimType:
| HTTP | When |
|---|---|
| 401 | Missing or invalid bearer token — detail names the reason (e.g. Admin access token required). |
| 403 | The token lacks the scim:read / scim:write capability for the method — detail is missing capability: scim:read (or scim:write). |
| 404 | User/Group/Schema not found. (A console-managed group is a deliberate 404 to a SCIM caller — no leak.) |
| 412 | A conditional write whose If-Match ETag doesn't match the current version (resource has changed). |
A
removeonmemberswithout a target is rejected as400 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.
error | kind | Meaning |
|---|---|---|
signature_required | — | One or more of the required signed-request headers (X-Device-Id, X-Device-Key-Id, X-Timestamp, X-Signature) is missing. |
unauthorized | bad_signed_headers | A device/key id isn't a valid UUID, the timestamp isn't parseable, or the signature isn't valid base64. |
unauthorized | clock_skew | The X-Timestamp is outside the allowed skew window. Recoverable — retry after an NTP sync. |
unauthorized | device_key_revoked | The device key is unknown/revoked, or the user is suspended or deleted. Terminal for the SDK — wipe and re-enroll. |
unauthorized | unsupported_key_alg | The key's registered algorithm isn't verifiable by this build. |
unauthorized | bad_signature | The signature doesn't verify (against the current key or, during a rotation overlap, the previous key). |
unauthorized | replayed_request | This exact signed request was already used — a verbatim replay. |
Protocol version floor
| Code | HTTP | Meaning |
|---|---|---|
upgrade_required | 426 | The 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. |
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.
PIN and registration
| Code | HTTP | Meaning |
|---|---|---|
pin_invalid | 401 | Wrong PIN. The body carries attempts_left. |
pin_locked | 403 | PIN attempts exhausted, or the code is already locked. |
pin_expired | 410 | No active PIN, or the PIN's TTL has expired. |
cooldown_active | 429 | A PIN-resend cooldown is in effect. Carries a Retry-After header. |
token_invalid | 401 | A registration/signup token is unknown, consumed, or unverified. |
token_expired | 410 | A registration/signup token is past its TTL. |
email_exists | 409 | Self-signup on an already-enrolled email. |
App attestation
When attestation is enforced, every attestation failure returns 403 with the machine error field forbidden and the message attestation_failed. 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"
}
]
}erroris alwaysvalidation_errorandmessageis alwaysRequest validation failed— branch ondetails[], never on the top-level message.details[].fieldis the request-body field the entry is about. For a rule that spans several fields it is the first offending one (thelocis rewritten to["body", <field>]too, so a client that only knowslocplaces it the same way).details[].codeis 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, …).typerepeats it. Branch oncode;msgis 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:
code | Meaning |
|---|---|
extra_forbidden | The 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_field | The 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_moved | The 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
code | Refused because |
|---|---|
fapi.requires_confidential_client | client_type is not confidential. |
fapi.requires_pkce | require_pkce is not true. |
fapi.requires_key_or_certificate_auth | token_endpoint_auth_method is not private_key_jwt, tls_client_auth or self_signed_tls_client_auth. |
fapi.requires_sender_constrained_tokens | Neither dpop_bound_access_tokens nor tls_client_certificate_bound_access_tokens is on. |
fapi.forbids_rs256 | id_token_signed_response_alg is RS256. |
fapi.requires_code_only | response_types is not exactly ["code"]. |
fapi.requires_https_redirect_uris | An allowed_redirect_uris entry is http and not a loopback address. msg names the offending URI. |
fapi.forbids_ciba_push | backchannel_token_delivery_mode is push. |
fapi.inherited_from_tenant | The tenant is a FAPI 2.0/CIBA tenant; its applications cannot set another profile or clear it. |
fapi_ciba.* — a FAPI application with a CIBA delivery mode
code | Refused because |
|---|---|
fapi_ciba.requires_signing_keys | No jwks / jwks_uri on file, so its signed backchannel requests could not be verified. |
fapi_ciba.requires_certificate_binding | tls_client_certificate_bound_access_tokens is off. |
jar.* — signed request objects
code | Refused because |
|---|---|
jar.requires_keys | require_signed_request_object is on with no jwks / jwks_uri to verify against. |
jwks.* — registered client keys
code | Refused because |
|---|---|
jwks.requires_keyed_method | jwks / 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_source | A private_key_jwt application must register exactly one of jwks / jwks_uri. |
jwks.one_source | A mutual TLS application may register one of them, or neither — not both. |
jwks.unreachable | The 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.invalid | The inline jwks is not a valid JWK Set. |
jwks.too_large | Larger than 32 KB. |
jwks.too_many_keys | More than 16 keys. |
jwks.kid_required | A key has no kid. |
jwks.duplicate_kid | Two keys share a kid. |
jwks.private_material | A key carries private members (d, p, q, dp, dq, qi, oth, k). Private material is never stored. |
jwks.alg_mismatch | A key's alg isn't allowed for its kty — ES256/ES384 for EC, PS256/RS256 for RSA. |
mtls.* — mutual TLS
code | Refused because |
|---|---|
mtls.listener_unavailable | This 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_required | tls_client_auth without tls_client_auth_subject_dn. |
mtls.trust_anchors_required | tls_client_auth without tls_client_auth_ca_pem. |
mtls.subject_dn_invalid | tls_client_auth_subject_dn is not parseable as an RFC 4514 distinguished name. |
mtls.certificate_required | self_signed_tls_client_auth without tls_client_certificate_pem. |
mtls.certificate_not_applicable | tls_client_certificate_pem sent for tls_client_auth. |
mtls.pki_fields_not_applicable | tls_client_auth_subject_dn / tls_client_auth_ca_pem sent for self_signed_tls_client_auth. |
mtls.fields_require_mtls_method | Any mutual TLS registration field sent for a method that is not a mutual TLS one. |
ciba.* — CIBA registration
code | Refused because |
|---|---|
ciba.requires_client_auth | CIBA is not available to a spa (public) client. |
ciba.endpoint_required | ping/push delivery without backchannel_client_notification_endpoint. |
ciba.user_code_unsupported | backchannel_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
code | Refused because |
|---|---|
backchannel_logout_initiators.invalid | A value outside the vocabulary (rp-logout, session-revoked, access-revoked, account-deleted, email-identifier-changed, session-expired). |
backchannel_logout_initiators.requires_rp_logout | The list does not contain rp-logout, which is always on. Refused rather than silently repaired. |
response_types.*, redirect_uris.*, url.* — request shape
code | Refused because |
|---|---|
response_types.invalid | A response_types entry this issuer does not offer (a bare token, an unknown word). |
redirect_uris.required | allowed_redirect_uris is empty — at least one is required. |
redirect_uris.invalid | A allowed_redirect_uris or post_logout_redirect_uris entry is not a usable absolute URL. msg names the entry and the reason. |
url.https_required | A URL-shaped field is not an absolute https URL — jwks_uri, backchannel_client_notification_endpoint, backchannel_logout_uri, terms_url, privacy_url. |
url.invalid | terms_url / privacy_url is not a usable absolute URL. |
url.private_host | A 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. |
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.
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.
Related
- Rate limits — the throttling policy and the
429shape. - Troubleshooting — mapping common errors to fixes.
- Glossary — terms used above.
Reference
Reference material for SenseCrypt — the error-code taxonomy, the rate-limit policy, token validation, CORS, a glossary, and a troubleshooting guide.
Rate limits
SenseCrypt rate-limit policy for the protocol endpoints you integrate against — the per-action windows and ceilings you need to design retries, the 429 response shape with Retry-After, how the client IP is determined, and how to handle limits.