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.
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: …).
frame_replayed400A liveness frame was already scored, or appears twice in one request. 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.
missing_version_header / unknown_version400The API version header is missing or not a known version.
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.

SDK error handling

Both SDKs wrap the envelope in a typed error class.

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.ErrCodeTokenExpired:
// Don't retry; SDK already refreshed and retried once.
// If you see this, the new token is also expired.
}
log.Printf("ekyc error %s (request_id=%s): %s",
ekycErr.Code, ekycErr.RequestID, ekycErr.Message)
}
}

Flutter:

try {
final r = await sdk.liveness.check(/* ... */);
} on EkycError catch (e) {
if (e.code == EkycErrorCode.rateLimited && e.retryAfter != null) {
await Future.delayed(e.retryAfter!);
}
print('ekyc error ${e.code} (requestId=${e.requestId}): ${e.message}');
}

Retry policy

The SDK retries network errors and 5xx automatically (3 attempts, exponential backoff). It does NOT retry 4xx — those are your bug, not a transient blip. Specifically:

  • 5xx: SDK retries with the same Idempotency-Key, fresh X-Request-Id per attempt.
  • 401 with token_expired / invalid_token (Flutter only): one refresh + one retry. Subsequent failures surface to caller.
  • 401 with invalid_credentials (Go): terminal. Fix your config.
  • 429: NOT auto-retried. Surface to caller with RetryAfter populated.