Skip to Content
🔐 Faable AuthError Codes

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_code is 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.
  • message is 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.
  • details is optional and structured. validation_error and already_exists carry fields; validation_error also carries issues (one { path, message } per failing field).
  • docs links 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.

CodeStatus
bad_request400
validation_error400
unauthorized401
payment_required402
forbidden403
not_found404
method_not_allowed405
conflict409
payload_too_large413
unsupported_media_type415
unprocessable_entity422
too_many_requests429
internal_error500

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.

CodeStatusMeaning
already_exists400A resource with the same unique field already exists (for a user, usually email). details.fields names the field.
already_member400The user is already a member of the team.
ambiguous_connection400Several connections match; pass connection_id.
bad_request400The request is malformed. message says what is wrong.
connection_misconfigured400The connection is missing settings needed to talk to its provider.
connection_required400No connection was given and the client has no default_connection.
expired_link400The magic link token was not found or has expired.
factor_already_confirmed400The factor was already confirmed.
factor_not_found400No factor with that id belongs to this user.
flow_not_found400The login flow does not exist in this account.
identity_orphaned400The identity pointed at a user that no longer exists; it was removed. Sign in again.
invalid_client400The client_id in the request does not name a client of this account.
invalid_client_metadata400Dynamic client registration rejected a field (RFC 7591). message names it.
invalid_code400The verification code is wrong, expired or already used.
invalid_connection400The connection does not exist, is disabled, or is not of the type this flow needs.
invalid_credentials400The email/username or password is incorrect.
invalid_device_code400The device or user code is invalid or expired.
invalid_email400The email address is not valid.
invalid_expand400An ?expand= path is not allowed on this resource.
invalid_factor400The factor cannot be verified (no secret material).
invalid_id400The id in the path is not a valid id for this resource.
invalid_link400The magic link is malformed.
invalid_phone400The phone number is not E.164 and could not be resolved with the account default country. Include the country code.
invalid_query400The ?query= FaableQL expression could not be parsed.
invalid_redirect_uri400The redirect URI is not registered for the client.
invalid_request400A required OAuth parameter is missing or two of them are incompatible. message says which.
invalid_role400The role belongs to another account.
invalid_state400The state is missing, expired, or does not describe a resumable step.
invalid_ticket400The ticket does not exist or is not the kind this endpoint accepts.
invite_used400The invite was already accepted.
malformed_credential400The WebAuthn credential is malformed.
no_passkey_enrolled400The user has no passkey to sign in with.
no_provider_token400The identity holds no provider access token.
no_refresh_token400The identity holds no provider refresh token, so it cannot be refreshed.
no_usable_factor400The user has no confirmed second factor to challenge.
passkey_login_disabled400Sign in with a passkey is not enabled for this client.
passkey_verification_failed400The WebAuthn response could not be verified.
password_too_weak400The password does not meet the connection policy. message lists each unmet rule.
passwordless_unavailable400No passwordless connection is configured or enabled for this client.
phone_changed400The phone on the user changed after the code was sent. Start again.
provider_error400The upstream identity provider returned an error. message carries it.
same_email400The new email is the same as the current one.
search_not_supported400This resource does not support ?search=.
team_required400An Auth Account must belong to a project (team).
ticket_expired400The ticket is past its expiry.
ticket_used400The ticket was already consumed.
user_has_no_email400The operation needs an email but the user has none.
validation_error400The body, query or path failed schema validation. details.issues lists each failing field.
access_denied401The caller may not access this row (ownership or machine-to-machine restriction).
action_unavailable401The Action that paused this login is disabled or belongs elsewhere.
client_credentials_denied401An Action denied the client_credentials grant. message carries the reason.
client_mismatch401The client belongs to a different account.
invalid_otp401The one-time password is wrong or expired.
invalid_passkey401The passkey assertion did not verify.
invalid_token401The bearer token is missing, malformed, expired or has no subject.
login_denied401A 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_code401The authenticator code did not verify.
mfa_invalid_recovery_code401The recovery code did not verify.
mfa_pending401A second-factor challenge is pending; answer it before continuing.
not_logged_in401There is no signed-in session for this request.
session_missing401The session that started this login no longer exists.
state_mismatch401The state belongs to another account, session, client or ceremony.
totp_not_allowed401This account does not accept authenticator apps.
unauthorized401The request carries no valid credentials.
unknown_passkey401The passkey is not enrolled for this user.
payment_required402The feature is not included in the current plan.
plan_required402The setting requires a higher plan. message says which.
audience_not_found403The audience does not name an API registered in this account.
forbidden403The credentials are valid but do not allow this operation.
insufficient_scope403The token lacks a scope this operation requires. message names it.
interaction_required403An Action asked for a redirect on a non-interactive flow.
mfa_required403The login needs a second factor that this flow cannot collect, or one is still pending on the session.
not_owner403The caller may only perform this operation on their own user.
origin_not_allowed403The request Origin is not in the client’s Allowed Web Origins.
signup_denied403A pre-signup Action or an active block denied the sign-up. message carries the reason.
signup_disabled403Self-service sign-up is disabled on this connection.
step_up_required403Confirm an existing factor before changing your factors.
system_resource403The resource is managed by Faable and cannot be modified or deleted.
user_suspended403The user is suspended and cannot sign in or be modified.
account_not_found404No Auth Account matches the request (domain, header or token).
client_not_found404No client with that client_id.
credential_not_found404The user has no password credential on this connection.
invite_not_found404No invite with that id in this account.
not_a_member404The user is not a member of the team.
not_found404The resource does not exist in this account.
revision_not_found404The revision is not in the flow history.
role_not_found404No role with that id in this account.
ticket_not_found404No ticket with that id belongs to this user.
method_not_allowed405The HTTP method is not supported on this path.
account_limit_reached409The project already has the maximum number of Auth Accounts.
conflict409The request conflicts with the current state of the resource.
email_taken409Another user in this account already uses that email.
identity_already_linked409That external identity is already linked to another user.
identity_conflict409The user already has a linked identity for this connection.
identity_link_denied409That external identity cannot be linked to this user.
phone_already_verified409The user already has a verified phone; the number cannot be swapped from the screen.
sms_failed409The SMS provider rejected the message.
sms_unavailable409This account cannot send SMS right now. The suffix says why: sms_unavailable:plan, :quota, :provider.
payload_too_large413The request body exceeds the size limit.
unsupported_media_type415The Content-Type is not accepted by this endpoint.
unprocessable_entity422The request is well-formed but cannot be processed.
too_many_attempts429Too many wrong codes. Wait a few minutes and try again.
too_many_requests429Rate limit exceeded. Honour the Retry-After header.
internal_error500Unexpected server error. Retry later; the request id is logged.
login_flow_misconfigured500The login flow of this account cannot run. message carries the node error.

Last updated on