Skip to main content

Errors

LumoAuth uses standard HTTP response codes. The error body shape depends on which endpoint family you hit: three JSON contracts for the REST and OAuth surfaces, plus the SCIM error schema on /scim2.0. There is no problem-details (application/problem+json) format anywhere in the API. Match your parser to the family below.

HTTP Status Codes​

CodeMeaningAction
200 OKRequest succeeded.—
201 CreatedResource was created.—
204 No ContentSucceeded with no response body (common for DELETE).—
400 Bad RequestMalformed request or invalid parameters.Fix the request
401 UnauthorizedMissing or invalid credentials/token.Refresh token or re-authenticate
403 ForbiddenAuthenticated but not authorized.Check scopes and permissions
404 Not FoundResource does not exist.Check the endpoint URL and org ID
409 ConflictResource already exists (e.g. duplicate email).Read or update instead of create
422 Unprocessable EntityValidation errors on the submitted data.See details field
429 Too Many RequestsRate limit exceeded.Back off — check Retry-After header
500 Internal Server ErrorUnexpected server error.Retry with exponential backoff
503 Service UnavailableTemporarily unavailable (maintenance, overload).Retry later

The three error-body contracts​

ContractShapeWhich endpoints
1. OAuth-style{ "error", "error_description" }Protocol endpoints: OAuth 2.0 / OIDC (authorize, token, introspection), AAuth (/aauth/*), JIT (/jit/*), MCP token/challenge errors, dynamic client registration. This is the OAuth 2.0 error shape.
2. Flat{ "error" } — sometimes just a code, sometimes with an HTTP-status echoSome agent / MCP protocol responses (e.g. JIT surfaces flat { "error": "<code>" } such as invalid_token, request_not_found). Distinguish success from error by the HTTP status or the presence of error.
3. Rich envelope{ "error", "message", "status", "details" }The admin REST API and the validation layer (/admin/*, resource CRUD, form/field validation). details maps each invalid field to a message, or carries structured context such as rejected_keys or required_scopes.
SCIM{ "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], "status", "detail" } with Content-Type: application/scim+jsonEverything under /orgs/{orgId}/api/v1/scim2.0/**, including authentication failures (401) and rate limiting (429). This is the SCIM 2.0 error shape. status is a string.

Notes:

  • The flat and OAuth-style contracts overlap: a flat { "error": "invalid_token" } and an OAuth { "error": "invalid_token", "error_description": "…" } share the error code — the difference is whether error_description is present. Read error for the machine code; treat error_description / message as optional.
  • The rich envelope's status field mirrors the HTTP status code; don't rely on it being present outside the admin/validation layer.

Examples​

1. OAuth-style (OAuth / AAuth / JIT / MCP)
{
"error": "invalid_client",
"error_description": "Client authentication failed"
}
2. Flat (some agent / MCP protocol responses)
{
"error": "request_not_found"
}
3. Rich envelope (admin API + validation)
{
"error": "validation_error",
"message": "Validation failed",
"status": 400,
"details": {
"email": "Invalid email format",
"password": "Must be at least 8 characters"
}
}
SCIM 2.0 error schema
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
"status": "401",
"detail": "Invalid or expired credentials"
}
Writing a client

Read error first — it's the one field present in every contract except SCIM (read detail there). Fall back to error_description then message for a human-readable string, and read details only for the rich validation envelope. The error-handling example below does exactly this.

Common API Error Codes​

errorStatusDescription
validation_error400 / 422One or more request fields failed validation. See details.
invalid_request400Missing or malformed request parameter.
invalid_client401OAuth client authentication failed.
invalid_token401Access token is missing, expired, or invalid.
unauthorized401No credentials provided.
invalid_api_key401The lmk_ API key is missing, malformed, expired, revoked, for another organization, or its creator is no longer active.
insufficient_user_authentication401The access token's auth_time is too old for this action (a step-up authentication challenge). Re-authenticate with the max_age from WWW-Authenticate. See MFA authentication.
forbidden403Admin API: the credential is valid but not allowed for this request. See Admin API authentication and authorization errors.
access_denied403Authenticated but insufficient privileges (OAuth surface).
insufficient_scope403Token scope does not cover this resource (OAuth surface).
feature_not_in_plan / plan_limit_reached403The organization's plan doesn't include this feature (for example ABAC or Zanzibar/ReBAC authoring, which needs the Fine-grained authorization add-on) or its limit is used up. The body names the feature or limit and an upgrade object with the plan or addon that unlocks it. See When a plan lacks a feature.
access_policy_denied403An agent credential was blocked by a conditional access policy; reason carries the policy code.
not_found404Resource does not exist.
conflict409Resource already exists.
rate_limit_exceeded429Too many requests. Retry after Retry-After seconds.
rate_limited429The per-credential limit on the Admin API, authorization and /me APIs was reached. Retry after Retry-After seconds. See Rate limits.
server_error500Unexpected internal error.
temporarily_unavailable503Service temporarily unavailable.

Admin API authentication and authorization errors​

The Admin API (/orgs/{orgId}/api/v1/admin/**) uses the rich envelope. Two codes cover almost every authentication problem; read message (and details when present) to tell the causes apart.

401 unauthorized / 401 invalid_api_key / 401 invalid_token​

No usable credential. WWW-Authenticate names the scheme that was expected (ApiKey realm="Admin API" or Bearer realm="API"). Common causes: no header, an expired or revoked key, an API key sent as Authorization: Bearer, an ID token instead of an access token (typ is not at+jwt), or a DPoP-bound token presented without a valid proof.

403 forbidden​

The credential is genuine, but this request is not allowed. The message states which rule failed:

Causemessage starts withdetails
Missing resource scope (API key or Bearer token)Insufficient scope: this API key is missing a required scope or … this access token is missing a required scoperequired_scopes, granted_scopes
Read-only credential on a mutating requestAccess denied: this API key has no admin write scope for this operationnone
Bearer token without any admin scopeAccess denied: this access token was not granted an admin scopenone
User or key creator lacks the endpoint's permissionAccess denied: missing required permission or Access denied: the API key owner does not hold the required permissionrequired_permissions
Target user holds permissions the caller lacks (password, e-mail, activation, block, delete, authenticators, temporary access code)You cannot perform this action on an account that holds privileges you do not have.none
Key creator deactivated, blocked or deletedAccess denied: the API key owner is no longer activenone
Endpoint accepts no credentials at allThis endpoint does not accept API keys or access tokens.none
Session caller without a CSRF witness or from a foreign originCSRF protection: …none
Bearer token on a session-only page (/portal, /account)Bearer access tokens are not accepted on this endpoint; it requires a browser session. (WWW-Authenticate: … error_description="bearer_not_accepted")none
API key on a session-only pageerror_description: API keys are not accepted on this endpoint; it requires a browser session. (WWW-Authenticate: ApiKey … error_description="api_key_not_accepted")none
403 forbidden (missing scope)
{
"error": "forbidden",
"message": "Insufficient scope: this API key is missing a required scope",
"status": 403,
"details": {
"required_scopes": ["admin:users:read"],
"granted_scopes": ["admin:webhooks:read"],
"key_scopes": ["admin:webhooks:read"]
}
}

key_scopes duplicates granted_scopes for older API-key consumers.

403 forbidden (missing permission)
{
"error": "forbidden",
"message": "Access denied: missing required permission",
"status": 403,
"details": {
"required_permissions": ["users.delete"]
}
}

required_permissions is any-of: holding one of the listed permissions is enough. Which permission each endpoint needs is listed under Required permissions.

400 invalid_request with rejected_keys​

PATCH /orgs/{orgId}/api/v1/admin/organization accepts only an allow-list of settings keys. If the payload contains anything else, or any nested key ending in _hash, _secret, _token, _key or _password, nothing is written and the response lists every offending path:

400 invalid_request (settings allow-list)
{
"error": "invalid_request",
"message": "Unsupported settings key(s): sandbox, security.scim_token. Writable keys are: display_name, displayName, timezone, locale, security, email, audit_log_retention_days, audit_log_auto_delete, agent_governance.",
"status": 400,
"details": {
"rejected_keys": ["sandbox", "security.scim_token"]
}
}

See Organization settings for the allow-list.

409 conflict​

Returned when a permission slug or an OAuth scope name already exists in the organization (or shadows a system-defined one). See Permissions.

OAuth 2.0 Errors​

Returned by the authorization, token, and introspection endpoints. The core OAuth 2.0 standard defines these endpoints, the grant types, and the error body shape shown below (error plus an optional error_description).

errorStatusCommon cause
invalid_request400Missing grant_type, invalid JSON body, unsupported content type
invalid_client401Wrong client_id, wrong client_secret, client not in organization
invalid_grant400Authorization code expired or reused, refresh token revoked
unauthorized_client403Client is not authorized for this grant type (400 from the CIBA and device authorization endpoints)
unsupported_grant_type400Grant type typo or not enabled
invalid_scope400Scope not registered or exceeds client's allowed scopes
access_denied403User denied consent, or policy blocked the request
server_error500Internal server error
temporarily_unavailable503Maintenance or overload

Authorization API Errors​

errorStatusDescription
MISSING_PERMISSION400Required field permission is missing
INVALID_PERMISSIONS400permissions must be a non-empty array
MISSING_FIELDS400Zanzibar check missing object, relation, or user
CHECK_FAILED500Permission check failed due to server error
ZANZIBAR_CHECK_FAILED400Invalid tuple format or unknown namespace
LIST_FAILED500Failed to retrieve user permissions

Agent & Token Exchange Errors​

errorStatusDescription
invalid_target400Token exchange target (audience) is invalid
unsupported_token_type400Revocation endpoint: token_type_hint is not supported. Token exchange never uses it: an unknown or mismatched subject_token_type / actor_token_type is invalid_grant (see Subject and actor token types)
delegation_not_allowed403Client is not configured to allow token exchange
budget_exceeded429Agent has exceeded its configured budget limits
capability_denied403Agent lacks the required capability
workload_identity_failed401Failed to verify workload identity token

Dynamic Client Registration Errors​

errorStatusDescription
invalid_redirect_uri400One or more redirect URIs use a non-allowed scheme
access_denied401No valid initial access token
access_denied403Dynamic registration is disabled for this organization
invalid_client_metadata400Client metadata is invalid or unsupported
invalid_token401Wrong registration_access_token on the client configuration endpoint

Validation Errors​

When a validation_error is returned, the details object maps each invalid field to a human-readable message:

422 Validation Error
{
"error": "validation_error",
"message": "Validation failed",
"status": 422,
"details": {
"name": "Name is required",
"redirectUris[0]": "Must be a valid HTTPS URL"
}
}

Rate Limit Errors​

The per-credential limit on the Admin API, /api/v1/authz/**, Zanzibar expand, ABAC check-bulk and /me/** answers with the rich envelope:

429 per-credential limit
{
"error": "rate_limited",
"message": "Too many requests for this credential",
"status": 429
}

Other limiters answer:

429 Rate Limit Exceeded
{
"error": "rate_limit_exceeded",
"message": "Too many requests. Please retry after 30 seconds.",
"status": 429,
"retryAfter": 30
}

The response also includes the Retry-After HTTP header with the number of seconds to wait.

Error-handling example​

import requests, time

class LumoAuthClient:
MAX_RETRIES = 3

def call_api(self, method, url, **kwargs):
for attempt in range(self.MAX_RETRIES):
response = requests.request(method, url, **kwargs)
if response.ok:
return response.json()
if response.status_code == 401:
self.refresh_token()
continue
if response.status_code == 429:
time.sleep(int(response.headers.get("Retry-After", 60)))
continue
if response.status_code >= 500:
time.sleep(2 ** attempt)
continue
err = response.json()
raise LumoAuthError(code=err.get("error"), message=err.get("message") or err.get("error_description"))
raise LumoAuthError(code="max_retries", message="Max retries exceeded")

Standards used on this page​

Plain nameStandard
OAuth 2.0 (the core sign-in and token protocol)RFC 6749
SCIM 2.0 (user and group provisioning)RFC 7642, RFC 7643 and RFC 7644
Step-up authentication challengeRFC 9470
Sender-constrained tokens (DPoP)RFC 9449
JWT access token profileRFC 9068
Dynamic client registrationRFC 7591 and RFC 7592

See the standards reference for what each one covers and how LumoAuth uses it.