This page covers the problems you are most likely to hit while wiring up Authbound for the first time. For a lookup table of every API error code, see Errors.

Authentication

Secret key rejected (401)

The X-Authbound-Key header is missing, or its value is not a live secret key. The code tells you which: Fix:
  • Use a key that starts with sk_test_ or sk_live_, from the same project and environment you are testing.
  • Send it in the X-Authbound-Key header from server-side code only.
  • If the key was revoked, create a new one in the dashboard. Secret keys are shown only once.
AuthboundClient checks the prefix when you construct it, so a malformed key throws AuthboundClientError with code INVALID_API_KEY_FORMAT before any request is sent.

Browser status returns 401

Browser status checks do not use your secret key. They use the per-verification clientToken plus your publishable key.
If you write the status request yourself, send both headers. The browser SDKs handle this for you. A missing token returns missing_token. An expired token returns invalid_token; client tokens last as long as the verification, 15 minutes by default. A publishable key from a different project than the verification returns 403 key_mismatch.

Verification

Policy not found (400)

Creating a verification returns invalid_policy. The policy_id must resolve to either a built-in preset or an active custom policy owned by the project associated with your secret key. Fix:
  • For a built-in preset, use its published ID, for example PolicyPresets.AGE_GATE_18 -> pol_age_over_18_authbound_v1. Presets work across projects and may not appear in the project’s policy list.
  • For a custom policy, use an active ID owned by the same project as the secret key, for example pol_age_gate_18_a1b2c3d4_v1. Copy it from the dashboard or create it with @authbound/server or POST /v1/policies.

Verification not found (404)

The API returns not_found: the verificationId does not exist for this project. Fix:
  • Verify you are using the same project for the create call and the status call (same secret/publishable key pair).
  • If the verification expired, create a new one. Verifications are short-lived.

Verification expired

Verifications and their clientToken expire after 15 minutes by default. After that, the verification reports status expired, and requests made with its client token return 401 invalid_token. Create a new verification and render the new wallet handoff.

VERIFICATION_BINDING_REQUIRED

Session finalization failed because the pending browser binding cookie is missing or expired. The SDK sets a __authbound_pending cookie when a verification is created. This cookie is capped at 10 minutes, regardless of your session cookie settings. The session route requires this cookie to match the verification being finalized. Fix:
  • Ask the user to restart verification if they spent more than 10 minutes in their wallet before returning to your app.
  • Ensure the browser can receive cookies from your verification create route (same origin, no cross-site cookie blocking).
  • If you need longer handoff windows, set sessionMode: "manual" and coordinate session creation on your server with webhooks or signed results.

Failed verification failure_code

When a verification reaches failed, the response includes a failure_code. Non-failed responses never include one. Common codes: See verification overview for the full list.

Local timeout or unavailable realtime events

The browser can reach its polling deadline before the API reports expired. Restart the handoff and reconcile durable access on the server. Custom subscribeToStatus calls should pass the create response expiresAt; otherwise polling has a five-minute default ceiling. For 503 realtime_unavailable, let the SDK fall back to polling. If you disabled fallback, poll the status endpoint with the same client-token and publishable-key headers.

Credential issuance

Credential definition not found (404)

You created an offer that references a credentialDefinitionId (or vct) that the project behind your secret key cannot resolve. Fix:
  • Create the credential definition first with authbound.issuer.credentialDefinitions.create({ ... }), then create the offer.
  • If you reuse a definition across requests, look it up first and create only when missing. See credential definitions for the idempotent pattern.
  • If you are creating an offer by vct, make sure the issuer for this project has a definition configured for that vct.

Definition already exists (409)

POST /v1/issuer/credential-definitions returns 409 with credential_definition_conflict when a definition with the same credentialDefinitionId already exists in this project. Fix:
  • Catch the conflict and call get(credentialDefinitionId) to load the existing record.
  • The Node.js example in Issue credentials shows this try get → fall back to create pattern.

Offer rejected (400)

The API returns validation_error: the offer request body failed validation. The message describes the first problem found. Common causes:
  • Missing claims object.
  • Sending neither credentialDefinitionId nor vct. The API requires exactly one of the two.
  • Sending non-string values in offer metadata. Offer metadata accepts strings only.
  • Sending claim paths that do not match the definition. Claims must use nested objects under the same path segments declared by the definition.

SDK and runtime

session_coordination_unsupported

SDK-managed browser sessions use the Web Locks API to prevent create and finalize races. Same-origin tabs share a lock keyed by the resolved endpoint origin. The SDK fails closed with session_coordination_unsupported when navigator.locks is missing or a lock cannot be acquired. Fix:
  • Use a browser that supports the Web Locks API.
  • For embedded webviews or other runtimes without Web Locks, set sessionMode to manual and create the trusted session on your server.
  • Use manual mode when several origins share a parent-domain cookie. Web Locks do not coordinate across browser origins, so cross-origin session mutation still belongs on the server.

Verification start is already in progress

Repeated start() calls share the pending request only when their options match. A concurrent call with different policyId, customerUserRef, metadata, or provider fails with verification_invalid_state. Keep metadata and provider objects stable when multiple components can start the same flow, or ensure only one component owns the start action.

Missing exports from an @authbound/* package

Your installed SDK is older than these docs. Upgrade every Authbound package together; they are released as one set:
Then restart your dev server.

Sending REST payloads directly

The SDKs always use camelCase (policyId, customerUserRef, credentialDefinitionId). The REST API’s casing depends on the resource: Check the API reference for the exact field names. A required field sent in the wrong casing counts as missing and returns 400 validation_error.

Need more help

If an error is not listed here, send the response body (object, code, message) and the request you made (without your secret key) to [email protected].