code, see Errors.
Authentication
Secret key rejected (401)
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_orsk_live_, from the same project and environment you are testing. - Send it in the
X-Authbound-Keyheader 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-verificationclientToken plus your publishable key.
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 returnsinvalid_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/serverorPOST /v1/policies.
Verification not found (404)
The API returnsnot_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 theirclientToken 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 reportsexpired. 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)
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 thatvct.
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 createpattern.
Offer rejected (400)
The API returnsvalidation_error: the offer request body failed validation. The message describes the first problem found. Common causes:
- Missing
claimsobject. - Sending neither
credentialDefinitionIdnorvct. 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
sessionModetomanualand 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
Repeatedstart() 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:
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].