Documentation
Errors & status codes
Every error U-verify returns is machine-readable and has a human sentence attached; you should never have to guess what went wrong from an HTTP status code alone.
The error envelope
Almost every error comes back in the same shape:
{
"success": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "This project does not have sufficient wallet balance to run this verification.",
"balance": 150,
"cost": 200
}
}- Show
error.messageto your users where relevant, it's already written to be understood by someone who isn't a developer. - Branch your own logic on
error.code, noterror.message: the wording can be improved over time, the code is stable. - Validation errors additionally carry
error.details: a map of{ field: ["message", ...] }.
Authentication & authorization
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
MISSING_HEADERS | 401 | X-Client-Key, X-Timestamp, or X-Signature wasn't sent. | Check your signing code attaches all three headers on every request. |
INVALID_CLIENT_KEY | 401 | The client_key doesn't exist, or your project is suspended. | Double-check the key, or check your dashboard for a suspension notice. |
TIMESTAMP_DRIFT | 401 | X-Timestamp is more than 5 minutes off the server's clock. | Sync your server's clock via NTP, this is the #1 cause in production. |
INVALID_SIGNATURE | 401 | The recomputed signature doesn't match. | See the signature-mismatch checklist on the Authentication page. |
REPLAY_DETECTED | 429 | The exact same signed request (client_key, timestamp, method, path, and body) was already processed once, within the last 10 minutes. | Each request needs its own fresh timestamp and signature; never resend a captured request verbatim, including for manual retries. Generate a new signed request instead. Two genuinely different requests issued in the same wall-clock second (e.g. two parallel GETs) won't collide, method and path are part of what's checked. |
CREDENTIAL_ERROR | 500 | The server couldn't resolve your stored secret. | Not your fault; retry, then contact support if it persists. |
TOKEN_MISSING | 401 | No Authorization: Bearer header on an endpoint that needs one. | Attach your access_token, in addition to the HMAC headers. |
TOKEN_EXPIRED | 401 | Your JWT access token's expires_in window has passed. | Call /auth/token/refresh before this happens, don't wait for a 401. |
TOKEN_INVALID | 401 | The token is malformed, revoked, or signed with an old key. | Get a fresh token via /auth/token. |
FORBIDDEN | 403 | Authenticated, but not allowed to touch this resource. | You're likely trying to access another client's data, check the ID you're passing. |
Validation & rate limits
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
VALIDATION_ERROR | 422 | One or more fields failed validation. | Read error.details; it's a map of field name → array of messages, same shape as Laravel's default validator. |
NOT_FOUND | 404 | The ulid/id in the URL doesn't exist (or doesn't belong to you). | Check the identifier, and remember most resources are scoped to your own client_key. |
TOO_MANY_ATTEMPTS | 429 | You've hit a rate limit. | Back off and retry; see the rate limit table on the API reference page. Respect it; repeated bursts can trigger a temporary IP block. |
Verification flow
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
INSUFFICIENT_BALANCE | 402 | Your wallet can't cover this check's cost. | Top up via POST /wallet/fund/initialize, or the Wallet page in your dashboard. |
CHECK_NOT_PERMITTED | 403 | Your billing plan doesn't include this check type. | Contact support to upgrade your plan. |
CHECK_ALREADY_PROCESSING | 409 | You submitted the same check twice while the first was still running. | Poll the status endpoint instead of resubmitting. |
CHECK_NOT_REQUIRED | 422 | You're submitting a check type this session didn't ask for. | Match the check against required_checks from when you created the session. |
CANNOT_CANCEL | 422 | The session already completed or was already cancelled. | No action needed, it's already in a final state. |
SESSION_EXPIRED | 410 | The verification session's TTL ran out before you finished it. | Start a new session, treat this as a normal retry path, not an alarming error. |
SESSION_NOT_FOUND | 404 | No session matches that session_id. | Check the ID, sessions from the hosted flow are single-use. |
LIVENESS_FAILED | 422 | Face Collections authenticate/search: the passive anti-spoofing check didn't pass before the identity match ever ran. | Re-capture with better lighting and make sure it's a live selfie, not a photo or screen. |
DOCUMENT_COMPARE_FAILED | 422 | POST /face-document-compare couldn't complete the comparison. | Check both images are clear, well-lit, and actually contain a face/ID photo. |
Hosted verification
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
INVALID_REDIRECT_URL | 400 | redirect_url isn't on a domain registered against this client_key. | Register the domain (including staging) from your dashboard, or ask support. |
CLIENT_INACTIVE | 400 | The project this client_key belongs to is suspended. | Contact support. |
CALLBACK_SIGNING_FAILED | 500 | U-verify couldn't sign the result, an internal issue. | Not your fault, retry the flow, contact support if it repeats. |
RESULT_NOT_FOUND | 404 | No stored result for that session_id / reference. | Double-check you're passing the exact session_id or reference from the original flow. |
Payments
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
PAYSTACK_ERROR | 502 | Paystack itself failed to initialize the transaction. | Retry in a moment,this is upstream, not something you can fix by changing your request. |
PAYMENT_FAILED | 402 | The wallet top-up transaction wasn't successful. | The user should retry payment, no funds were moved. |
PAYMENT_NOT_VERIFIED | 402 | Paystack hasn't confirmed the public-search payment as successful yet. | You haven't been charged, try the search again. |
UNDERPAYMENT | 402 | The amount paid doesn't match the check's fee. | Shouldn't happen through the normal UI, contact support if you see this. |
PROVIDER_FAILED | 502 | Payment succeeded but the government/bank source didn't return a result. | You'll be refunded, contact support with the reference if it doesn't happen automatically. |
Still stuck?
If you're getting an error that isn't listed here, or the fix above didn't work, don't spend hours guessing, reach a real person with the exact error.code and, if you can, the X-Timestamp of the failing request.