Errors and retries
Every non-2xx response uses the same envelope:
{
"error": {
"type": "validation_error",
"message": "burst mode requires at least 3 frames",
"request_id": "01a07991-79d5-76da-a6bc-1020aa49ba28",
"param": "image_<n>",
"doc_url": "https://docs.drukverify.com/errors/validation_error"
}
}
type is the machine-readable error code. request_id uniquely
identifies the failing request — include it when contacting support.
Error catalogue
| Type | HTTP status | Meaning |
|---|---|---|
missing_authorization | 401 | No Authorization header. |
invalid_key | 401 | The dv_sk_* key is malformed or unknown. Terminal. |
revoked_key | 401 | The key was revoked in the dashboard. Terminal. |
expired_token | 401 | The dv_tok_* session token is past its TTL. Mint a new one and retry. |
invalid_signature | 401 | The session token's signature does not verify. Re-mint. |
invalid_credentials | 401 | The credential could not be verified at all. Fix the configuration; do not retry. |
unauthorized | 401 | The credential is valid but not allowed here (for example a token used where a key is required). |
forbidden | 403 | The credential's role does not permit the action. |
tenant_suspended / tenant_archived | 403 | The tenant is not active. Contact ops. |
feature_disabled / feature_not_assigned / feature_paused | 403 | The feature behind this endpoint is not enabled for your plan, or has been paused. |
validation_error | 400 | A request field is malformed or missing; param names it. Also returned for uploads over 10 MB (upload rejected: …). |
missing_version_header / unknown_version | 400 | The Drukverify-Version header is missing or not a known version. |
frame_replayed | 400 | A liveness frame was already scored, by bytes or by decoded pixels, or appears twice in one request. Capture fresh frames; to retry a timed-out upload, resend with the same Idempotency-Key. See Liveness checks. |
challenge_token_invalid | 400 | The liveness challenge_token is unknown, expired or already used. |
not_found / registration_not_found | 404 | No object with that id in your tenant and mode. |
idempotency_conflict | 409 | The Idempotency-Key was reused with a different request body. |
idempotency_in_progress | 409 | The same idempotent request is still being processed. Retry shortly. |
model_version_mismatch | 409 | A face embedding from an older model version cannot be compared. Re-register. |
verification_closed | 409 | The verification session is completed, or its attempt limit is exhausted. |
rate_limited | 429 | Per-key rate limit hit. Sleep retry_after_ms, then retry. |
quota_hard_cap_exceeded | 429 | The tenant's monthly hard cap for this feature is reached. |
internal_error | 500 | Platform fault. Safe to retry with backoff. |
service_unavailable | 503 | A downstream service is unreachable. Safe to retry with backoff. |
A response can also carry version_deprecated as a warning on a
successful (200) call when the requested API version is on its way out.
Input errors from the inference services
Image inputs are validated by the service that scores them, and those
rejections use the same envelope with these type values. They are 400s
(413 for image_too_large) and mean "retake", not "retry".
| Type | Endpoint | Meaning |
|---|---|---|
unsupported_image_format | all | Not a decodable JPEG, PNG, WebP, TIFF, BMP or HEIC (front or back; including a truncated or corrupt file), a PDF sent as the back, or the image claims absurd dimensions. |
image_too_large | ocr, fraud | A side decodes to more than 40 megapixels. Resize it (phone photos are 12-24 MP) and resend. |
no_face, multiple_faces, low_image_quality | liveness | No face, more than one comparably sized face, or the face crop failed the size, brightness or blur gate. low_image_quality carries reasons (face_too_small, too_dark, too_bright, too_blurry). |
no_face_detected, multiple_faces_detected, low_quality_face | face | The same conditions on a face verification, registration or search image; a suffix names the image (…_in_image_b). |
OCR and fraud do not reject a readable document as an error: they return a
200 with ocr_eligible: false or a retake / incomplete decision. See
Document scanning and
Fraud detection.
SDK error handling
Each SDK wraps the envelope in a typed error.
Go:
result, err := client.Liveness.Check(ctx, req)
if err != nil {
var ekycErr *ekyc.Error
if errors.As(err, &ekycErr) {
switch ekycErr.Code {
case ekyc.ErrCodeRateLimited:
time.Sleep(ekycErr.RetryAfter)
case ekyc.ErrCodeInvalidCredentials:
log.Fatal("API key revoked or wrong; check config")
}
log.Printf("ekyc error %s (request_id=%s): %s",
ekycErr.Code, ekycErr.RequestID, ekycErr.Message)
}
}
Flutter:
try {
final r = await client.livenessCheckChallenge(frames, challengeToken: token);
} on EkycRateLimitException catch (e) {
await Future.delayed(e.retryAfter ?? const Duration(seconds: 1));
} on EkycException catch (e) {
print('ekyc error ${e.type} (requestId=${e.requestId}): ${e.message}');
}
JavaScript:
try {
await client.liveness.check(frames, { mode: 'challenge', challengeToken });
} catch (e) {
if (e instanceof EkycError) console.log(e.status, e.body?.error?.type);
}
Retry policy
The SDKs do not retry on their own, with two exceptions, so plan retries in your code:
- Flutter refreshes the session token once on a
401 expired_tokenand repeats the request, and the liveness widget retries a timed-out upload once with the sameIdempotency-Key. - Go retries transport-level failures (DNS, connection refused,
timeout) before returning
network_error.
What to do by status:
- 5xx and 503: retry with backoff and the same
Idempotency-Key. - 429: sleep for
retry_after_ms, then retry. - 401
expired_token: mint a new session token. - 401
invalid_key/revoked_key/invalid_credentials: terminal; fix the configuration. - 400: never retry the same request; the message names what to change.