Errors and retries
Every non-2xx response uses the same envelope:
{
"error": {
"type": "validation_failed",
"message": "image must be at least 256×256",
"request_id": "req_2f8d4c91d9c14bc2b0f8a4d62a3b7c11",
"param": "image",
"doc_url": "https://docs.drukverify.com/errors/validation_failed"
}
}
type is the machine-readable error code. request_id uniquely
identifies the failing request — include it when contacting support.
Common types
| Type | HTTP status | Meaning |
|---|---|---|
invalid_credentials | 401 | The dv_sk_* is wrong or revoked. Terminal. |
token_expired | 401 | The dv_tok_* is past its TTL. Refresh and retry. |
invalid_token | 401 | The dv_tok_* was revoked or never existed. Re-mint. |
validation_failed | 400 | A request field is malformed. param names the field. |
payload_too_large | 413 | Image bytes exceed MAX_IMAGE_BYTES. Resize client-side. |
rate_limited | 429 | Per-key rate limit hit. Sleep retry_after_ms, retry. |
payment_required | 402 | Tenant is over plan limits or has a billing issue. |
internal_error | 5xx | Platform fault. Safe to retry with backoff. |
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, freshX-Request-Idper 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
RetryAfterpopulated.