Skip to main content

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​

TypeHTTP statusMeaning
missing_authorization401No Authorization header.
invalid_key401The dv_sk_* key is malformed or unknown. Terminal.
revoked_key401The key was revoked in the dashboard. Terminal.
expired_token401The dv_tok_* session token is past its TTL. Mint a new one and retry.
invalid_signature401The session token's signature does not verify. Re-mint.
invalid_credentials401The credential could not be verified at all. Fix the configuration; do not retry.
unauthorized401The credential is valid but not allowed here (for example a token used where a key is required).
forbidden403The credential's role does not permit the action.
tenant_suspended / tenant_archived403The tenant is not active. Contact ops.
feature_disabled / feature_not_assigned / feature_paused403The feature behind this endpoint is not enabled for your plan, or has been paused.
validation_error400A request field is malformed or missing; param names it. Also returned for uploads over 10 MB (upload rejected: …).
missing_version_header / unknown_version400The Drukverify-Version header is missing or not a known version.
frame_replayed400A 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_invalid400The liveness challenge_token is unknown, expired or already used.
not_found / registration_not_found404No object with that id in your tenant and mode.
idempotency_conflict409The Idempotency-Key was reused with a different request body.
idempotency_in_progress409The same idempotent request is still being processed. Retry shortly.
model_version_mismatch409A face embedding from an older model version cannot be compared. Re-register.
verification_closed409The verification session is completed, or its attempt limit is exhausted.
rate_limited429Per-key rate limit hit. Sleep retry_after_ms, then retry.
quota_hard_cap_exceeded429The tenant's monthly hard cap for this feature is reached.
internal_error500Platform fault. Safe to retry with backoff.
service_unavailable503A 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".

TypeEndpointMeaning
unsupported_image_formatallNot 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_largeocr, fraudA side decodes to more than 40 megapixels. Resize it (phone photos are 12-24 MP) and resend.
no_face, multiple_faces, low_image_qualitylivenessNo 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_facefaceThe 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_token and repeats the request, and the liveness widget retries a timed-out upload once with the same Idempotency-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.