BluAuth
Docs
Sign in
User FAQ
  • Reset my password
  • I can't sign in
  • Didn't get reset email
  • Account linking
  • Session expiry
  • Two-factor auth
Admin Guides
Theme Studio
  • Overview
  • Layouts
  • Styling tokens
  • Concept copy
  • Assets & backgrounds
  • Advanced CSS
Admin Shell
  • Users
  • Providers
  • Clients
  • Invitations
Integrations
  • OIDC flow
  • Legacy OAuth flow
  • Provider token brokering
  • Email triggers
  • Webhook events
  • Session contract
Reference
  • API
  • Error codes
  • Event shapes
  • Design tokens
Runbooks
  • Deployment
  • Local operations

Error Codes

BluAuth's JSON APIs return failures using a single, versioned envelope. The full list of codes is defined in shared/types/api-errors.ts and produced by the factory methods in shared/utils/api-error.ts.

{
    "success": false,
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "Human-readable detail",
        "status": 400,
        "requestId": "req_01H...",
        "fields": { "email": { "code": "invalid_format", "message": "Invalid email" } }
    }
}

requestId is always present — log it on the caller side so Sentry issues can be correlated to specific client failures. fields is present only on VALIDATION_ERROR responses derived from Zod issues. debug (with stack, cause, timestamp, service, version) is attached in non-production stages.

Rules of engagement

  • Log code, not message. code is stable across releases; message is human-readable and may be changed or translated.
  • Do not parse message. Localize against code on the caller side if you need translation.
  • On RATE_LIMITED honor Retry-After. Do not retry instantly — the limiter uses a moving window.
  • Distinguish 401 vs 403. 401 = "we don't know who you are, re-authenticate." 403 = "we do, but you can't do that." They require different UX and telemetry.
  • Retry only idempotent requests on 5xx. INTERNAL_ERROR, SERVICE_UNAVAILABLE, REAUTH_REQUIRED, UPSTREAM_PROVIDER_ERROR are transient-looking but have different recovery semantics — see the section on each.

Code reference

The canonical list from ErrorCode in shared/types/api-errors.ts:

CodeHTTPFactoryCategory
VALIDATION_ERROR400ApiError.validation(...)Client / validation
AUTHENTICATION_REQUIRED401ApiError.unauthorized(...)Auth
PERMISSION_DENIED403ApiError.forbidden(...)Auth
RESOURCE_NOT_FOUND404ApiError.notFound(...)Not found
RESOURCE_CONFLICT409ApiError.conflict(...)Conflict
RATE_LIMITED429(emitted by rate limiter)Rate limit
INTERNAL_ERROR500ApiError.internal(...)Server
SERVICE_UNAVAILABLE503ApiError.serviceUnavailable()Server
BUSINESS_RULE_VIOLATION422ApiError.businessRule(...)Client / semantics
INVALID_TOKEN401ApiError.invalidToken(...)Provider token brokering
UNAUTHORIZED_CLIENT403ApiError.unauthorizedClient()Provider token brokering
NO_LINKED_ACCOUNT404ApiError.noLinkedAccount()Provider token brokering
INSUFFICIENT_SCOPE403ApiError.insufficientScope()Provider token brokering
REAUTH_REQUIRED502ApiError.reauthRequired()Provider token brokering
UPSTREAM_PROVIDER_ERROR502ApiError.upstreamProviderError()Provider token brokering

Field error codes

fields[<path>].code from FieldErrorCode:

CodeMeaning
requiredMissing field that Zod flagged required
invalid_typeWrong JS type (string/number/boolean mismatch)
too_smallBelow the minimum length / numeric bound
too_largeAbove the maximum length / numeric bound
invalid_formatFailed .email(), .uuid(), .url(), regex, etc.
invalid_enumNot one of z.enum([...]) allowed values
customCustom .refine(...) validator

fields[<path>] may also include received (the supplied value) and expected (a human description).


By category

Validation — client-side fixes required

VALIDATION_ERROR (400)

Raised by ApiError.validation(...) when the payload fails Zod parsing or when a handler needs to reject malformed input before hitting the database. fields enumerates every failed path.

{
    "success": false,
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "Validation failed",
        "status": 400,
        "requestId": "req_01H...",
        "fields": {
            "email": { "code": "invalid_format", "message": "Invalid email" },
            "password": { "code": "too_small", "message": "Password must be at least 8 characters." }
        }
    }
}

Resolve: surface fields[...] under the appropriate form controls. Re-submit with corrected values. Do not retry identical payloads.

BUSINESS_RULE_VIOLATION (422)

Raised by ApiError.businessRule(...) when the request is syntactically valid but violates a domain invariant. Example: attempting to reset a password for a user who only signs in via enterprise SSO when allowPasswordForSsoUsers = false.

Resolve: read message for the specific rule. Either adjust the request (e.g. link a credential account first) or relax the rule in service_settings.

Authentication & authorization

AUTHENTICATION_REQUIRED (401)

No session cookie, invalid session token, or an expired Better Auth session.

{
    "success": false,
    "error": { "code": "AUTHENTICATION_REQUIRED", "message": "Authentication required", "status": 401, "requestId": "req_..." }
}

Resolve: redirect the user to /login (or the hosted login with a client-bound client_slug). Pop the session cookie.

PERMISSION_DENIED (403)

Authenticated, but role or scope insufficient. Emitted by requireAdminRole when a non-admin hits /api/admin/**, or by resource-scoped checks that accept an owner but not the requester.

Resolve: do not retry with the same credentials. Escalate to an admin, or request additional scopes via a re-authorize flow.

Not found

RESOURCE_NOT_FOUND (404)

Raised by ApiError.notFound(resource, id?). message follows the pattern "<resource> with id '<id>' not found" or "<resource> not found".

{
    "success": false,
    "error": { "code": "RESOURCE_NOT_FOUND", "message": "User with id '...' not found", "status": 404, "requestId": "req_..." }
}

Resolve: verify the ID. Do not retry.

Conflict

RESOURCE_CONFLICT (409)

Raised by ApiError.conflict(...) when the write conflicts with existing state. The admin user-create endpoint returns a special 409 with data payload containing the existing user's id, authMethod (sso or password), and linkedProviders so the caller can decide whether to link.

{
    "success": false,
    "error": { "code": "RESOURCE_CONFLICT", "message": "User already exists as an enterprise SSO account", "status": 409, "requestId": "req_..." },
    "data": { "id": "...", "email": "...", "authMethod": "sso", "linkedProviders": ["google"], "hasCredentialAccount": false }
}

Resolve: inspect data, then link the existing account instead of creating a new one.

Rate limit

RATE_LIMITED (429)

Emitted by the platform-level rate limiter (per-IP or per-session depending on the route). Always accompanied by a Retry-After header.

Resolve: honor Retry-After. If repeated, back off exponentially. Excessive 429s for the same principal is a signal to stop — not to retry harder.

Server errors

INTERNAL_ERROR (500)

Raised by ApiError.internal(...) or surfaces from an unhandled exception. Sentry was notified and the request is tagged with requestId.

Resolve: surface a generic error to the user, file the requestId if reproducible. Safe to retry idempotent GETs after a short delay.

SERVICE_UNAVAILABLE (503)

Raised by ApiError.serviceUnavailable(...) — typically when a dependency is down (Valkey, SES, KMS) or the process is still warming. /api/health returns 503 with { status: "unhealthy" } when DB or cache checks fail.

Resolve: retry with exponential backoff. If persistent, check CloudWatch alarms and the health endpoint.

Provider token brokering

These codes only appear on /api/provider-tokens/:provider. They distinguish client-authorization, user-linkage, and upstream failures. See the provider token brokering guide for full flow.

INVALID_TOKEN (401)

Missing or unknown Authorization: Bearer on /api/provider-tokens/:provider.

{
    "success": false,
    "error": { "code": "INVALID_TOKEN", "message": "Access token is expired or invalid", "status": 401, "requestId": "req_..." }
}

Resolve: re-fetch a fresh BluAuth access token via the authorization-code flow.

UNAUTHORIZED_CLIENT (403)

The authenticating client is not listed in the provider's allow-list (oauth_clients.allowedProviderTokens).

{
    "success": false,
    "error": { "code": "UNAUTHORIZED_CLIENT", "message": "Client is not authorized to access provider tokens for 'google'", "status": 403, "requestId": "req_..." }
}

Resolve: have an admin add the provider slug to the client's allowedProviderTokens.

NO_LINKED_ACCOUNT (404)

The authenticated user has no row in accounts for the requested provider. Common on first-time use.

Resolve: bounce the user through /oauth2/login?provider=<slug>&intent=link to connect the account.

INSUFFICIENT_SCOPE (403)

A grant exists but does not cover the requiredScopes in the request body. message includes both granted and required sets.

{
    "success": false,
    "error": {
        "code": "INSUFFICIENT_SCOPE",
        "message": "Insufficient scopes for provider 'google'. Granted: [openid, email]. Required: [openid, email, https://www.googleapis.com/auth/calendar.readonly]",
        "status": 403,
        "requestId": "req_..."
    }
}

Resolve: bounce the user through /oauth2/login?provider=<slug>&client_id=<id>&additional_scopes=<space-delimited> to upgrade the grant.

REAUTH_REQUIRED (502)

Upstream refresh token was revoked or is otherwise unusable. Bluauth does not silently create a new grant — the user must re-authenticate.

Resolve: bounce the user through /oauth2/login?provider=<slug>&intent=link. Treat as a non-retryable 502.

UPSTREAM_PROVIDER_ERROR (502)

The upstream IdP returned a transient failure during refresh (5xx, network timeout, etc.).

Resolve: retry with backoff. Persistent failures should be escalated — check provider status pages.


Protocol-endpoint errors

OIDC (/api/oidc/**), legacy OAuth (/oauth2/**), and discovery (/.well-known/**) routes follow their respective specs, not this envelope.

  • OIDC authorization / token endpoints return {"error": "invalid_grant", "error_description": "..."} per OIDC Core §3.1.2.6 and OAuth 2.0 §5.2. Common codes: invalid_request, invalid_client, invalid_grant, unauthorized_client, unsupported_grant_type, invalid_scope.
  • /api/oidc/token/introspect always returns 200 with {"active": true|false} per RFC 7662.
  • /api/oidc/token/revoke always returns 200 with {"ok": true} per RFC 7009.
  • **/.well-known/**** returns 200 with the document or 404 if not applicable.
  • /oauth2/login and /oauth2/callback redirect with an error query parameter on failure (consent_denied, provider_error, missing_params, invalid_state, exchange_failed, session_failed, link_session_mismatch, redirect_not_allowed). The reason parameter narrows session_failed / link_failed cases (account_claimed_by_other_user, provider_already_linked, ambiguous_email_alias, user_not_found, user_create_failed, internal).

These routes never return the BluAuth JSON envelope, by design — they must be consumable by standards-compliant OAuth/OIDC clients.

On this page

  • Rules of engagement
  • Code reference
  • Field error codes
  • By category
  • Validation — client-side fixes required
  • Authentication & authorization
  • Not found
  • Conflict
  • Rate limit
  • Server errors
  • Provider token brokering
  • Protocol-endpoint errors
DocsPrivacyTerms
© 2026 Blu Digital Group