Errors

Every error uses the same shape and a meaningful HTTP status. This page lists what each one means and what your integration should do about it.

Error shape

json
{
  "error": "invalid_request",
  "message": "The authorization code has already been used.",
  "status": 400
}

Messages are written for developers and are safe to log — they never contain personal data or key material. They are not stable strings, though: branch on status, not on message text.

Status codes

StatusMeaningRetry?What to do
400Invalid request — malformed body, missing field, or an expired or already-used codeNoFix the request. If the code was already used, read the session instead of exchanging again.
401Missing, malformed, unknown or revoked API keyNoCheck the Authorization header and that the key is still active. Retrying will not help.
403Valid key, but not permitted — a publishable key on the callback, or a claim outside your entitlementNoUse a secret key server-side. For claims, contact us about an expanded entitlement.
404Session not found, or it belongs to another accountNoCheck the sessionId. Cross-tenant reads are reported as not found by design.
429Rate limit exceeded for this key or IPYesBack off using Retry-After. See rate limits.
503The identity provider is unavailableYesBack off using Retry-After. This is not a verification failure — do not tell the user they failed.
Document data capture

The document data-capture operations add 409 (a part already uploaded, or fields fetched before every part was uploaded), 411 (no declared length), 413 (body too large) and 422 EVIDENCE_UNREADABLE (retake). Their error codes and the retakeHint are listed on Document data capture.

Retryable vs terminal

The distinction matters more than the specific code:

  • Retryable — 429, 503. Something transient. Back off and try again; the same request will eventually succeed.
  • Terminal — 400, 401, 403, 404. Something about the request or its credentials is wrong. Retrying wastes quota and delays the user; fix the cause.
A retry policy that behaves
private static final Set<Integer> RETRYABLE = Set.of(429, 503);

<T> T withRetry(Supplier<T> call) {
    int attempt = 0;
    while (true) {
        try {
            return call.get();
        } catch (EmnetApiException e) {
            if (!RETRYABLE.contains(e.status()) || ++attempt >= 3) {
                throw e;                      // terminal, or out of attempts
            }
            sleep(backoffWithJitter(attempt)); // honour Retry-After when present
        }
    }
}
Never retry a code exchange

Authorization codes are single-use. If an exchange fails with 400, the code is spent — retrying always fails. Read the session to find out whether the verification actually completed, then ask the user to start again if it did not.

Common causes

403 on the callback endpoint

Almost always a publishable key being used server-side. The callback returns claims, so it accepts secret keys only. Check which key your server actually loaded — a copy-paste from a front-end config is the usual culprit.

401 that appeared suddenly

The key was revoked, or a deployment is loading the wrong environment’s secret. Check the key prefix in your configuration: a service that should be live but is holding sk_test_… is a misconfigured deployment, not an API problem.

400 “code already used”

Usually a duplicate callback — a user refreshing the page, or a retry loop. Make your callback handler idempotent: if you already recorded the verification for that state, return the stored result instead of exchanging again.

404 on a session you just created

The session belongs to a different account than the key you are reading with. This happens when a test key created the session and a live key is reading it, or vice versa.

What to show your users

API errors are for you, not for them. Map them to something a person can act on, and never surface a raw message or status code:

SituationA good message
400 — code expired or used“This verification link has expired. Please start again.”
503 — provider unavailable“Verification is temporarily unavailable. Please try again in a few minutes.”
429 — rate limited“We’re processing a lot of requests right now. Please try again shortly.”
401 / 403 — your configuration“Something went wrong on our side.” — then alert your own team.
A 401 or 403 is your bug, not the user’s

These mean your integration is misconfigured. Show a generic failure, and make sure it raises an alert with your team — a silently broken key can otherwise fail every verification for hours.

Getting help

Write to support@emnetid.com with the sessionId, the status code, and the approximate time of the request. Never send an API key or any personal data — we do not need either to investigate.