Skip to content
authreads Docs

Errors & rate limits

Every error uses one predictable shape, and every rate limit tells you when to try again. Handle these two contracts and your integration degrades gracefully instead of guessing.

The error shape

Failures return application/problem+json (RFC 7807). Always branch on the HTTP status first; use the stable code for programmatic handling and detail for logs — never parse title.

application/problem+json
{
  "status": 400,
  "title": "Bad Request",
  "code": "organization_type_not_resolved",
  "detail": "The user's organization type does not grant this application."
}
FieldAlways presentUse it for
statusYesThe HTTP status, repeated in the body.
titleYesA short human label — display, don't parse.
codeNoA stable machine string for branching in code.
detailNoA specific, loggable description of this instance.
Some errors add domain-specific fields. Organization-type resolution failures, for example, include requested and available so you can show the caller which org-types would have worked.

Stable code vocabulary

This is the authoritative list of Authreads machine codes. A code is published only when it changes a caller's recovery action; errors without one are handled by HTTP or OAuth status. Published values are additive and will not be renamed or reused.

CodeMeaningCaller action
invalid_credentialsThe login proof was rejected; account existence is deliberately undisclosed.Do not retry the same proof. Let the user retry or recover credentials.
rate_limitedA general request or hosted/OAuth limit was exceeded.Wait for Retry-After, add jitter, then retry.
login_temporarily_lockedThe account/network login brake or enforced tenant lockout fired.Wait for Retry-After before another sign-in attempt.
revoked · expired · user_disabled · tenant_disabled · not_foundA direct session is no longer usable, with the exact terminal reason.Clear the application session and require sign-in; disabled identities need administrator action.
session_activity_invalid · session_idle_timeout · session_activity_expiredSigned browser-activity state is invalid, idle, or past its absolute lifetime.End the session or restart authentication; never synthesize activity state.
app_unknown · app_disabled · organization_unknown · organization_inactive · no_membership · membership_inactive · no_role · role_denied · org_type_denied · package_unassigned · package_inactive · entitlement_missingThe access evaluator denied token issuance at the named policy layer.Correct tenant access configuration; do not retry unchanged input.
organization_type_not_resolvedThe requested organization-type key/environment pair is not configured.Use requested/available/available_total to choose or configure an exact pair.
enrolment_token_unknown · enrolment_token_revoked · enrolment_token_expired · enrolment_token_not_yet_valid · enrolment_token_exhaustedAn installation enrolment token cannot be used for the named lifecycle reason.Issue/use an appropriate token; only not-yet-valid becomes usable without replacement.
enrolment_token_rate_limitedThe enrolment token exceeded its per-minute budget.Back off before redeeming it again.
installation_key_duplicateThe tenant already has an installation with the submitted public key.Treat the redemption as a retry conflict; do not generate another ticket use implicitly.
installation_limit_reachedThe tenant reached its active installation capacity.Retire an installation or raise capacity before retrying.
enrolment_app_invalid · enrolment_workspace_invalidToken issuance named an app or workspace outside the target tenant.Correct the exact reference named by the code; never retry the unchanged identifier.
identity_source_issuer_duplicate · external_principal_duplicate · external_principal_already_claimedAn issuer, source-scoped subject, or live principal claim conflicts with retained identity state.Resolve the exact duplicate/claim condition; do not infer identity ownership from human-readable text.
identity_source_tenant_invalid · external_principal_source_invalid · identity_binding_principal_invalid · identity_binding_user_invalidAn external-identity write named a tenant-scoped reference that is not valid in the transaction tenant.Correct the exact reference named by the code.
installation_limit_above_maximum · installation_maximum_below_limitA capacity update would cross the operator-approved maximum or place the maximum below the active configured limit.Use the operator exception endpoint first, or lower the tenant limit before lowering its maximum.
installation_campaign_changedThe active membership of a deployment campaign changed after its blast radius was displayed.Refresh the campaign count, review the new impact, and require a fresh human confirmation; never retry unchanged.
service_overloadedThe public or private API plane exhausted its concurrency capacity.Retry with bounded exponential backoff and jitter.
OAuth's standard error remains the protocol-level category. Hosted login adds the same Authreads value as error_code; direct/BFF problem details usecode. The three login values are one typed server contract and are also emitted into OpenAPI as an enum, so adding one requires an explicit code and contract-test update.

Account-security errors

BFF account-security routes deliberately normalize an unknown, deleted, cross-tenant, or wrong-user-pool user/app pair to the same 404. This prevents the endpoint from becoming an identity-enumeration oracle. A missing account-security:read or account-security:manage scope returns 403.

“Revoke others” returns 404 when current_session_id is not a live account-session handle for that same tenant, user, and app. Repeating a completed individual revocation succeeds with { "revoked": 0 }; it is not an error and does not create another audit record.

Status codes you'll see

StatusMeaning & usual cause
400Malformed request — a missing or invalid body field.
401Bad tenant identity — wrong X-Tenant-Id, or the Bearer secret/token doesn't match this tenant.
403Authenticated but not allowed — the user's role ∩ org-type ∩ package doesn't grant this app, or the token's scope is insufficient.
404The resource doesn't exist in your tenant.
409Conflict — e.g. a uniqueness constraint on create.
422The request was well-formed but semantically invalid.
429Rate limited — back off and retry after the Retry-After delay.
503Service temporarily unavailable — retry only when the response explicitly permits it.

OAuth & token errors

The OAuth endpoints return standard OAuth error codes. These four cause the most confusion — here's what each really means:

ErrorWhat it usually means & the fix
invalid_grantOn /oauth/login/preflight, the description invalid credentials means the password-stage login was rejected without revealing whether the account exists. On token endpoints, the auth code may have been reused or expired (codes are single-use and short-lived), the redirect_uri didn't match at token exchange, the PKCE code_verifier was wrong, or a refresh token was expired/revoked. Don't retry a failed code exchange — it fails permanently; send the user through login again.
invalid_client

Client authentication failed. Confidential clients use exactly one of HTTP Basic (client_secret_basic) or form credentials (client_secret_post); presenting both is rejected. Public clients use PKCE with no secret.

On the client-credentials grant specifically (machine tokens), this one code covers three distinct causes, deliberately not distinguished — telling them apart would let a caller enumerate valid client ids:

  • the client_id doesn't exist;
  • the client_secret doesn't match that client_id;
  • the client, or its tenant, isn't active.

Check in that order: re-copy the client_idshown alongside the secret on the credential's reveal screen (both are shown together, once, specifically so you can confirm they're a matching pair before storing them separately); confirm the secret wasn't truncated or altered in transit into your secret manager; then confirm the credential and its tenant are both still active in the dashboard. A pair assembled from two different credential creations — id from one, secret from another — is structurally valid (right prefixes, right length) and fails exactly this way, with nothing to eyeball.

invalid_requestA required parameter is missing or duplicated — e.g. no code_challenge on authorize, or credentials sent two ways at once. On /oauth/authorize specifically, this is also what you get for any query parameter it doesn't recognize (including prompt and max_age) — see Known incompatibilities with common OIDC libraries if you're integrating a generic OIDC client.
invalid_tokenA token your API verified failed on signature, kid, iss, aud, or exp. Walk the checklist in Verify a token.

Login failures on direct and hosted flows

Both login planes preserve the same privacy boundary but use their native response format. A wrong password and an unknown email remain indistinguishable. A temporary lock is explicit because the caller needs a safe retry time.

PlaneCredentials rejectedTemporarily locked
Direct / BFF401, code invalid_credentials429, code login_temporarily_locked, plus Retry-After
Hosted / OIDC400 OAuth invalid_grant, error_code: invalid_credentials429 OAuth temporarily_unavailable, error_code: login_temporarily_locked, plus Retry-After
Tenant policy lockout and the always-on abuse brake intentionally share the locked response. They require the same recovery action, and identifying which threshold fired would disclose defensive configuration without helping the person sign in.

Password-change and re-verify failures

A wrong proof on POST /api/v1/auth/session/reverify (step-up, see Sessions & tokens) or POST /api/v1/auth/password/change returns a plain 401 Unauthorized with no code field — the legacy, uncoded shape (only status, title, and detail), not the invalid_credentials-coded shape login uses. Branch on the 401 status; don't look for a code here.

This 401 does not spend the login lockout budget — but don't retry it blindly either. Step-up and password-change failures are counted in their own rate buckets, keyed on tenant + user only. They're entirely separate from the account/network login-lockout counters, so a wrong step-up proof can never lock the user out of signing in, and a locked-out login can never block step-up. But the same distinction cuts the other way: this isn't a machine-credential 401 that's generically safe to retry once — it means the human's proof (password or OTP) was rejected, so retry only when the user supplies a new proof, never automatically. Each bucket allows five attempts per 15 minutes before it returns its own 429 with Retry-After.

Rate limits

authreads protects sensitive endpoints with independent brakes rather than one global quota. When you hit one, the response carries a Retry-After header (in seconds) telling you exactly when to try again:

  • Login brake — repeated failed logins are throttled per account and per network, so credential-stuffing slows to a crawl without locking a real user out permanently. Platform default: 5 failures per account in a 15-minute window trips a 15-minute lock.
  • Password-change & re-verify brakes — step-up and password-change flows have their own separate limits, isolated from the login brake above (see previous section). Platform default: 5 attempts per 15 minutes, keyed per tenant + user, for each of the two flows independently.
  • Token / OAuth brake — pre-auth OAuth requests are keyed on the client (or verified tenant), never the raw IP. Over-limit OAuth requests return 429 with error temporarily_unavailable and a Retry-After. Its error_code is rate_limited. Platform default: 300 token requests per client per 60 seconds.
Cache the machine token; don't mint one per call. At 300 requests/minute, a naive per-request mint exhausts the token brake under any real load. Reuse the access token until shortly before expires_in elapses — see Management API → Get a machine token. These are the platform's current configured values, not a contractual ceiling — they can change without notice. Always drive retry timing from Retry-After, never from a number hard-coded against this page.
429 Too Many Requests
HTTP/1.1 429 Too Many Requests
Retry-After: 42
Content-Type: application/problem+json

{ "status": 429, "title": "Too Many Requests",
  "code": "login_temporarily_locked",
  "detail": "Sign-in is temporarily locked." }
Honor Retry-After.It's the only rate-limit signal — there are no X-RateLimit-* headers. On a 429 or 503, wait the stated seconds (with jitter) before retrying; don't hammer.

A resilient client, in short

  • Branch on status, then on code — treat title/detail as display/log text.
  • Retry only 429/503 (and network errors), always respecting Retry-After with jitter.
  • On 401, re-check the tenant id and secret; on 403, check role ∩ org-type ∩ package and token scope.
  • Stuck on a specific symptom? The onboarding troubleshooting table maps common failures to causes.