API Error Codes - ericfitz/tmi GitHub Wiki

API Error Codes

Every TMI error response carries a machine-readable error code. The codes form a closed, documented vocabulary defined in the OpenAPI specification (Error and OAuthError schemas), so clients can branch on them. This page describes the vocabulary and how to use it.

Decision record: tmi#1048.

Overview

The vocabulary has three parts:

  1. REST codes (Error.error): a closed enum used by every REST endpoint. Each code names one HTTP semantic.
  2. Domain reasons (details.code): an open list of lowercase snake_case reasons a client can act on, nested under a REST code.
  3. Protocol codes (OAuthError.error): used only by /oauth2/*, /saml/* and /.well-known/*, where RFC-defined codes apply.

The HTTP status of a response and its error code always agree with the tables below. WebSocket message codes are a separate vocabulary and are not covered here.

Response shapes

REST error (Error schema). error and error_description are required; error_uri and details are optional.

{
  "error": "conflict",
  "error_description": "A group with this name already exists",
  "details": {
    "code": "duplicate_group",
    "suggestion": "Choose a different group name"
  }
}

Protocol error (OAuthError schema), with the same optional fields:

{
  "error": "invalid_grant",
  "error_description": "The authorization code is invalid or expired"
}

Branch on error first. Use details.code, when present, for finer handling. Do not parse error_description; it is for humans and can change.

REST error codes (Error.error)

Code HTTP status Meaning
invalid_input 400 A body, field, header or query value fails validation (including pagination bounds and JSON syntax)
invalid_id 400 A path or query identifier is malformed (UUID, slug)
invalid_patch 400 A JSON Patch document is malformed, targets a disallowed path, or cannot be applied. Resync and rebuild the patch rather than fixing a field
unauthorized 401 Missing, expired or invalid credentials
insufficient_user_authentication 401 Step-up authentication required (RFC 9470)
forbidden 403 Authenticated but not permitted, including protected objects and self-deletion
not_found 404 The resource or route does not exist, or is hidden by authorization
method_not_allowed 405 HTTP method not supported for the endpoint
not_acceptable 406 Requested representation not supported
conflict 409 State conflict: duplicate, in use, or wrong lifecycle state
version_mismatch 409 If-Match does not match the current version; reload and retry
gone 410 Permanently removed (for example, audit data past retention)
payload_too_large 413 Request body exceeds the size limit
unsupported_media_type 415 Content-Type not accepted
unprocessable_entity 422 Well-formed request that cannot be processed in the current state (for example, provider not registered, embedding dimension mismatch)
if_match_required 428 The If-Match header is missing
rate_limit_exceeded 429 Transient limit reached; honor Retry-After
quota_exceeded 403 or 429 Hard cap reached; retrying does not help
server_error 500 Unexpected failure, including panic recovery
not_implemented 501 Operation not supported by this server
service_unavailable 503 A dependency (database, cache, provider) is unavailable; retry later

Legacy top-level domain codes

Two domain codes also appear in the Error.error enum because deployed clients branch on the top-level field. Each is also set in details.code, so clients can migrate. They will move to details.code only in a future breaking change.

Code HTTP status Meaning
feature_not_available 404 The requested feature is not available on this server
content_token_provider_not_configured 422 The content token provider is not configured; the response also carries a top-level provider_id

Domain reasons (details.code)

details.code is an open list: new reasons can be added without a schema version change, so treat unknown values as the generic case for the REST code. Values are lowercase snake_case.

REST code (status) details.code values
invalid_input (400) picker_file_id_mismatch, invalid_picker_registration, invalid_challenge, invalid_provider_type, client_callback_required, client_callback_not_allowed, invalid_if_match, invalid_version, duplicate_header, unsupported_encoding, reserved_key
unauthorized (401) token_not_linked_or_failed, invalid_token (identity link token)
forbidden (403) protected_group, provider_mismatch
not_found (404) session_not_found, feature_not_available
conflict (409) duplicate_group, duplicate_membership, self_deletion, protected_user, deletion_blocked, encryption_not_enabled, unreadable_settings_limit, session_not_active
unprocessable_entity (422) provider_not_registered, provider_not_configured, content_token_provider_not_configured, dimension_mismatch, inconsistent_dimensions, no_source, access_request_not_supported
rate_limit_exceeded (429) too_many_connections, session_full, message_rate_limit, duplicate_invocation
quota_exceeded (429) session_limit_exceeded
server_error (500) websocket_upgrade_failed
service_unavailable (503) llm_busy, llm_not_configured

Protocol error codes (OAuthError.error)

These routes use the OAuthError schema: /oauth2/{authorize,token,refresh,revoke,introspect,userinfo,callback,step_up}, /saml/* and /.well-known/*. The TMI-defined routes /oauth2/content_callback, /oauth2/providers* and /me/identities/link/* use the REST codes above.

On these routes invalid_request means an RFC 6749 malformed protocol request, not a REST validation failure.

Group Codes
RFC 6749 invalid_request, invalid_client, invalid_grant, unauthorized_client, unsupported_grant_type, invalid_scope, access_denied, unsupported_response_type, server_error, temporarily_unavailable
RFC 7009 unsupported_token_type
RFC 9470 insufficient_user_authentication
TMI extensions (RFC 6749 section 8.5) identity_mismatch, account_conflict, email_not_verified, provider_unreachable, provider_response_invalid, invalid_provider
TMI SAML extensions saml_error, saml_not_enabled, saml_unavailable, saml_provider_not_found, saml_metadata_error, saml_init_error, saml_invalid_logout_request, saml_logout_error
Transport codes from route-agnostic middleware unauthorized, not_found, method_not_allowed, not_acceptable, payload_too_large, unsupported_media_type, rate_limit_exceeded, server_error

invalid_token and insufficient_scope (RFC 6750) are values in the WWW-Authenticate response header, not response body codes.

Request-validation failures from the Unicode validation middleware return invalid_request on protocol routes and invalid_input on REST routes.

Migration from earlier codes

Earlier releases emitted undocumented synonyms. HTTP statuses did not change. If your client matched any of the old values, update it as follows.

Old code New code
invalid_request (REST routes) invalid_input
validation_failed, validation_error, invalid_limit, invalid_offset invalid_input
invalid_uuid invalid_id
invalid_format, patch_failed invalid_patch
internal_error, internal_server_error server_error
too_many_requests rate_limit_exceeded
too_many_connections, session_full, message_rate_limit rate_limit_exceeded, with the old value in details.code
session_limit_exceeded quota_exceeded, with the old value in details.code
Identity link invalid_token (401) unauthorized, with invalid_token in details.code
Identity link invalid_provider invalid_input
Identity link temporarily_unavailable service_unavailable
Other domain-specific codes The REST code for the same HTTP status, with the old value in details.code (except the two legacy codes above)
Human sentences in the error field A vocabulary code, with the sentence in error_description

On protocol routes, too_many_requests became rate_limit_exceeded and service_unavailable became the RFC code temporarily_unavailable in token handling.

See also