Error codes
Every error response from the Management API and the hosted flows has the same shape:
{
"status": 400,
"message": "Invalid phone number. Include the country code, for example +34600123456.",
"error_code": "invalid_phone",
"docs": "https://faable.com/docs/auth/errors#invalid_phone",
"details": { "fields": ["phone"] }
}error_codeis the field to branch on. It is stable: a code is never renamed or reused with another meaning, and each code maps to exactly one HTTP status.messageis written for a person and may change between releases. Show it in a log; do not compare it, and do not show it to end users as it is — map the code to your own copy instead.detailsis optional and structured.validation_errorandalready_existscarryfields;validation_erroralso carriesissues(one{ path, message }per failing field).docslinks to the entry for the code on this page.
A few codes carry a detail after a colon — sms_unavailable:plan, sms_unavailable:quota. Branch on the prefix.
The one exception is the OAuth token endpoint (POST /oauth/token), which answers in the RFC 6749 shape — { "error", "error_description" } — and carries error_code as an extension member when the canonical error is too coarse (for example invalid_grant with error_code: "user_suspended").
With the SDK
@faable/auth-sdk throws a FaableApiError on any non-2xx. The code is on the error, and every generated method lists the codes it can throw in its docstring:
import { FaableApiError, FaableAuthApi } from '@faable/auth-sdk'
try {
await auth.userCreate({ name, email, phone })
} catch (e) {
if (e instanceof FaableApiError) {
switch (e.error_code) {
case 'already_exists':
return 'That email is already registered.'
case 'invalid_phone':
return 'Write the phone with its country code, e.g. +34 600 123 456.'
case 'validation_error':
return `Check ${e.details?.fields?.join(', ')}.`
}
if (e.isErrorCode('sms_unavailable'))
return 'SMS is not available right now.'
}
throw e
}ErrorCode is exported as a TypeScript union, so a switch over it is checked by the compiler.
The OpenAPI document
Every operation in /docs/json documents its error responses: one entry per status, with error_code restricted to the codes that operation can emit. Code generators pick them up from there.
Generic codes
When an error has no more specific code, error_code is derived from the status. You will always get one of these at minimum; the specific codes below are what you should reach for first.
| Code | Status |
|---|---|
bad_request | 400 |
validation_error | 400 |
unauthorized | 401 |
payment_required | 402 |
forbidden | 403 |
not_found | 404 |
method_not_allowed | 405 |
conflict | 409 |
payload_too_large | 413 |
unsupported_media_type | 415 |
unprocessable_entity | 422 |
too_many_requests | 429 |
internal_error | 500 |
All codes
Generated from the server’s catalogue (npm run dump-error-codes in the auth service); the list is the same one the OpenAPI ErrorCode schema enumerates.
| Code | Status | Meaning |
|---|---|---|
already_exists | 400 | A resource with the same unique field already exists (for a user, usually email). details.fields names the field. |
already_member | 400 | The user is already a member of the team. |
ambiguous_connection | 400 | Several connections match; pass connection_id. |
bad_request | 400 | The request is malformed. message says what is wrong. |
canonical_host_breaks_passkeys | 400 | canonical_host is outside the account’s current WebAuthn RP ID — every passkey already enrolled would stop working. Set webauthn_rp_id in the same request to a suffix that covers both. |
connection_misconfigured | 400 | The connection is missing settings needed to talk to its provider. |
connection_required | 400 | No connection was given and the client has no default_connection. |
expired_link | 400 | The magic link token was not found or has expired. |
factor_already_confirmed | 400 | The factor was already confirmed. |
factor_not_found | 400 | No factor with that id belongs to this user. |
flow_not_found | 400 | The login flow does not exist in this account. |
identity_orphaned | 400 | The identity pointed at a user that no longer exists; it was removed. Sign in again. |
invalid_canonical_host | 400 | canonical_host must be this account’s own platform host or one of its verified custom domains. |
invalid_client | 400 | The client_id in the request does not name a client of this account. |
invalid_client_metadata | 400 | A client field was rejected: by dynamic client registration (RFC 7591), or on a client create/update (initiate_login_uri). message names it. |
invalid_code | 400 | The verification code is wrong, expired or already used. |
invalid_connection | 400 | The connection does not exist, is disabled, or is not of the type this flow needs. |
invalid_credentials | 400 | The email/username or password is incorrect. |
invalid_current_password | 400 | The current password is wrong. |
invalid_device_code | 400 | The device or user code is invalid or expired. |
invalid_email | 400 | The email address is not valid. |
invalid_expand | 400 | An ?expand= path is not allowed on this resource. |
invalid_factor | 400 | The factor cannot be verified (no secret material). |
invalid_id | 400 | The id in the path is not a valid id for this resource. |
invalid_link | 400 | The magic link is malformed. |
invalid_name | 400 | The name contains a link. message says which field. |
invalid_password_hash | 400 | The imported password hash cannot be read, or its cost is above what a login can afford. message names the field. |
invalid_permission | 400 | A permission is not in the catalog of its API, or the API is not in this account. details.permissions lists the ones refused. See Roles and Permissions. |
invalid_phone | 400 | The phone number is not E.164 and could not be resolved with the account default country. Include the country code. |
invalid_profile_field | 400 | A profile field is not declared for this application, or its value does not match the declaration. message names the key. |
invalid_project_id | 400 | The project id is not of the form project_<hex> or team_<hex>. |
invalid_query | 400 | The ?query= FaableQL expression could not be parsed. |
invalid_redirect_uri | 400 | The redirect URI is not registered for the client. |
invalid_request | 400 | A required OAuth parameter is missing or two of them are incompatible. message says which. |
invalid_role | 400 | The role belongs to another account. |
invalid_state | 400 | The state is missing, expired, or does not describe a resumable step. |
invalid_ticket | 400 | The ticket does not exist or is not the kind this endpoint accepts. |
invite_used | 400 | The invite was already accepted. |
malformed_credential | 400 | The WebAuthn credential is malformed. |
no_database_connection | 400 | The tenant has no database connection, so there is nowhere to store a password. Create one and retry. |
no_passkey_enrolled | 400 | The user has no passkey to sign in with. |
no_provider_token | 400 | The identity holds no provider access token. |
no_refresh_token | 400 | The identity holds no provider refresh token, so it cannot be refreshed. |
no_usable_factor | 400 | The user has no confirmed second factor to challenge. |
oauth21_required | 400 | The tenant enforces OAuth 2.1 (oauth21_enabled) and the request is one OAuth 2.1 removed: a response_type other than code, an authorization request without PKCE (code_challenge with S256), or the password grant. message says which. Update the application (with @faable/auth-js, version 3.2.0 or later) or turn oauth21_enabled off for the tenant. |
passkey_login_disabled | 400 | Sign in with a passkey is not enabled for this client. |
passkey_verification_failed | 400 | The WebAuthn response could not be verified. |
password_too_weak | 400 | The password does not meet the connection policy. message lists each unmet rule. |
passwordless_unavailable | 400 | No passwordless connection is configured or enabled for this client. |
phone_changed | 400 | The phone on the user changed after the code was sent. Start again. |
provider_error | 400 | The upstream identity provider returned an error. The suffix is the provider’s own code, when it sent one: provider_error:bad_refresh_token (the user must re-authorize), provider_error:incorrect_client_credentials (our configuration, the user can do nothing). details.provider_error_description carries the provider’s sentence. |
same_email | 400 | The new email is the same as the current one. |
search_not_supported | 400 | This resource does not support ?search=. |
team_required | 400 | An Auth Account must belong to a project (team). |
ticket_expired | 400 | The ticket is past its expiry. |
ticket_used | 400 | The ticket was already consumed. |
user_has_no_email | 400 | The operation needs an email but the user has none. |
validation_error | 400 | The body, query or path failed schema validation. details.issues lists each failing field. |
weak_password_hash | 400 | The imported password hash uses a fast digest (MD5, SHA-1, SHA-2) or a cost below the minimum. message says which. |
action_unavailable | 401 | The Action that paused this login is disabled or belongs elsewhere. |
client_credentials_denied | 401 | An Action denied the client_credentials grant. message carries the reason. |
client_mismatch | 401 | The client belongs to a different account. |
invalid_otp | 401 | The one-time password is wrong or expired. |
invalid_passkey | 401 | The passkey assertion did not verify. |
invalid_token | 401 | The bearer token is missing, malformed, expired or has no subject. |
login_denied | 401 | A post-login Action denied the sign-in. message carries the reason it gave. On token grants it is a 403 with error: invalid_grant. |
mfa_invalid_code | 401 | The authenticator code did not verify. |
mfa_invalid_recovery_code | 401 | The recovery code did not verify. |
mfa_pending | 401 | A second-factor challenge is pending; answer it before continuing. |
not_logged_in | 401 | There is no signed-in session for this request. |
session_missing | 401 | The session that started this login no longer exists. |
state_mismatch | 401 | The state belongs to another account, session, client or ceremony. |
totp_not_allowed | 401 | This account does not accept authenticator apps. |
unauthorized | 401 | The request carries no valid credentials. |
unknown_passkey | 401 | The passkey is not enrolled for this user. |
m2m_quota_exceeded | 402 | The project used the machine-to-machine tokens its plan includes this month. Free is capped; Hobby and Pro are billed per token instead. |
payment_required | 402 | The feature is not included in the current plan. |
plan_required | 402 | The setting requires a higher plan. message says which. |
access_denied | 403 | The caller may not access this row (ownership or machine-to-machine restriction). |
audience_not_found | 403 | The audience does not name an API registered in this account. |
email_not_verified | 403 | The user’s email is not verified, and the operation needs it to be. |
forbidden | 403 | The credentials are valid but do not allow this operation. |
insufficient_scope | 403 | The token lacks a scope this operation requires. message names it. |
interaction_required | 403 | An Action asked for a redirect on a non-interactive flow. |
mfa_required | 403 | The login needs a second factor that this flow cannot collect, or one is still pending on the session. |
not_owner | 403 | The caller may only perform this operation on their own user. |
origin_not_allowed | 403 | The request Origin is not in the client’s Allowed Web Origins. |
reauth_required | 403 | The operation needs a recent sign-in (and the second factor, when the user has one). Sign in again with max_age=0 and retry with the new token. |
signup_denied | 403 | A pre-signup Action or an active block denied the sign-up. message carries the reason. |
signup_disabled | 403 | Self-service sign-up is disabled on this connection. |
step_up_required | 403 | Confirm an existing factor before changing your factors. |
system_resource | 403 | The resource is managed by Faable and cannot be modified or deleted. |
user_suspended | 403 | The user is suspended and cannot sign in or be modified. |
account_not_found | 404 | No Auth Account matches the request (domain, header or token). |
client_not_found | 404 | No client with that client_id. |
credential_not_found | 404 | The user has no password credential on this connection. |
invite_not_found | 404 | No invite with that id in this account. |
not_a_member | 404 | The user is not a member of the team. |
not_found | 404 | The resource does not exist in this account. |
revision_not_found | 404 | The revision is not in the flow history. |
role_not_found | 404 | No role with that id in this account. |
ticket_not_found | 404 | No ticket with that id belongs to this user. |
method_not_allowed | 405 | The HTTP method is not supported on this path. |
account_limit_reached | 409 | The project already has the maximum number of Auth Accounts. |
account_transfer_refused | 409 | The Auth Account cannot be moved to that project. details.blockers lists why. |
conflict | 409 | The request conflicts with the current state of the resource. |
email_taken | 409 | Another user in this account already uses that email. |
identity_already_linked | 409 | That external identity is already linked to another user. |
identity_conflict | 409 | The user already has a linked identity for this connection. |
identity_link_denied | 409 | That external identity cannot be linked to this user. |
last_sign_in_method | 409 | Unlinking this account would leave the user with no way to sign in: set a password, verify the email or link another account first. |
phone_already_verified | 409 | The user already has a verified phone; the number cannot be swapped from the screen. |
session_already_revoked | 409 | The session was already revoked. |
sms_failed | 409 | The SMS provider rejected the message. |
sms_unavailable | 409 | This account cannot send SMS right now. The suffix says why: sms_unavailable:plan, :quota, :provider. |
payload_too_large | 413 | The request body exceeds the size limit. |
unsupported_media_type | 415 | The Content-Type is not accepted by this endpoint. |
unprocessable_entity | 422 | The request is well-formed but cannot be processed. |
too_many_attempts | 429 | Too many wrong codes. Wait a few minutes and try again. |
too_many_login_attempts | 429 | Too many failed sign-in attempts for this email or username from this network. Wait a few minutes and try again. |
too_many_requests | 429 | Rate limit exceeded. Honour the Retry-After header. |
internal_error | 500 | Unexpected server error. Retry later; the request id is logged. |
login_flow_misconfigured | 500 | The login flow of this account cannot run. message carries the node error. |
Last updated on