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
{
"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
| Status | Meaning | Retry? | What to do |
|---|---|---|---|
400 | Invalid request — malformed body, missing field, or an expired or already-used code | No | Fix the request. If the code was already used, read the session instead of exchanging again. |
401 | Missing, malformed, unknown or revoked API key | No | Check the Authorization header and that the key is still active. Retrying will not help. |
403 | Valid key, but not permitted — a publishable key on the callback, or a claim outside your entitlement | No | Use a secret key server-side. For claims, contact us about an expanded entitlement. |
404 | Session not found, or it belongs to another account | No | Check the sessionId. Cross-tenant reads are reported as not found by design. |
429 | Rate limit exceeded for this key or IP | Yes | Back off using Retry-After. See rate limits. |
503 | The identity provider is unavailable | Yes | Back off using Retry-After. This is not a verification failure — do not tell the user they failed. |
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.
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
}
}
}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:
| Situation | A 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. |
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.