Errors
All Veridia API errors follow a consistent shape. This page is the canonical reference for every error code you might see.
Error response shape
Every error response includes:
{
"error": "invalid_body",
"message": "Request body failed validation",
"requestId": "9f511d92ac11236d-SJC",
"detail": {
"fieldErrors": {
"country": ["expected 2 characters"]
}
}
}
| Field | Type | Description |
|---|---|---|
error | string | Machine-readable error code — switch on this in your code |
message | string | Human-readable summary, mostly for logs |
requestId | string | Unique ID for this request — include it in support tickets |
detail | object | Optional. Extra context (field errors, mismatched key reason, etc.) |
The same requestId is also returned in the X-Request-Id response header — handy when JSON parsing fails.
Error catalog
invalid_body — 400 Bad Request
The request body failed Zod schema validation. Check detail.fieldErrors for specifics.
Common causes:
- Missing required field
- Wrong type (e.g., string where number expected)
- String too long (e.g.,
userRef > 128chars) - Invalid enum value (e.g.,
documentType: "id"instead of"dni") verificationIddoesn't match^vf_[A-Za-z0-9]{16,24}$
On /submit specifically, detail.reason may be:
| Reason | Meaning |
|---|---|
doc_front_key_mismatch | The keys.docFront doesn't match what /init returned |
selfie_key_mismatch | The keys.selfie doesn't match what /init returned |
doc_back_key_mismatch | The keys.docBack doesn't match what /init returned |
doc_front_not_uploaded | The R2 object for the front doesn't exist — client never uploaded |
selfie_not_uploaded | The R2 object for the selfie doesn't exist |
doc_front_disappeared | The R2 object existed during HEAD but was gone before OCR (extremely rare) |
Recovery: Fix the request and retry. These are client errors.
unauthorized — 401 Unauthorized
The Authorization header is missing, malformed, or doesn't use the Bearer scheme.
Recovery: Add a valid Authorization: Bearer <key> header.
invalid_key — 401 Unauthorized
The bearer token doesn't correspond to a valid key. Either it never existed, was revoked, or expired.
Recovery: Generate a new key in the dashboard. Check that you're using the right key for the right environment.
origin_not_allowed — 403 Forbidden
The publishable key was used from a domain that's not in its allowed origins list.
Recovery: Add your domain to the allowed origins list in the dashboard. For local development, add http://localhost:PORT.
wrong_environment — 403 Forbidden
A test key (qv_pub_test_* / qv_sec_test_*) was used with a live endpoint, or vice versa.
Recovery: Use the matching environment. Test keys for testing, live keys for production.
insufficient_credits — 403 Forbidden
The tenant has 0 credits. New verifications can't start.
Recovery: Top up credits in the dashboard, or contact support if you're on a custom plan.
verification_not_found — 404 Not Found
The verificationId doesn't exist, doesn't belong to this tenant, or the intent expired (>1 hour after /init).
Recovery:
- Check the ID for typos
- Make sure you're using the same API key (or another key from the same tenant)
- If more than 1 hour has passed since
/init, restart the flow
not_found — 404 Not Found
The route doesn't exist.
Recovery: Check the URL. Common typos: /v1/verifications instead of /v1/verify/:id.
rate_limited — 429 Too Many Requests
Hit the per-tenant rate limit. The Retry-After response header tells you how long to wait (in seconds).
| Endpoint | Default limit |
|---|---|
/v1/verify/init | 60 / minute |
/v1/verify/submit | 30 / minute |
/v1/verify/:id | 600 / minute |
Recovery:
- Wait for the duration in
Retry-After, then retry - Add throttling on your side
- Contact support to lift limits per-tenant
internal — 500 Internal Server Error
Something broke on the Veridia side. Rare.
Recovery:
- Retry with exponential backoff (the issue may be transient)
- If it persists, send a support ticket with the
requestIdfrom the response
Handling errors well
JavaScript / Node.js
async function callVeridia(url, options) {
const response = await fetch(url, options);
if (response.ok) {
return response.json();
}
const error = await response.json();
// Switch on the error code, not HTTP status
switch (error.error) {
case 'invalid_body':
console.error('Validation failed:', error.detail?.fieldErrors);
throw new ValidationError(error);
case 'rate_limited':
const retryAfter = response.headers.get('Retry-After') || 1;
await new Promise(r => setTimeout(r, retryAfter * 1000));
return callVeridia(url, options); // retry once
case 'insufficient_credits':
// Notify ops, fall back to manual review
await notifyCreditsExhausted();
throw new BusinessError(error);
case 'verification_not_found':
// Logical error — restart the flow
throw new NotFoundError(error);
case 'internal':
// Veridia issue — log and alert
logger.error('Veridia internal error', {
requestId: error.requestId,
url,
});
throw new ExternalServiceError(error);
default:
throw new Error(`Unhandled Veridia error: ${error.error}`);
}
}
Python
import time
import requests
def call_veridia(url, **kwargs):
response = requests.request(**kwargs, url=url)
if response.ok:
return response.json()
error = response.json()
if error["error"] == "invalid_body":
raise ValidationError(error)
if error["error"] == "rate_limited":
retry_after = int(response.headers.get("Retry-After", 1))
time.sleep(retry_after)
return call_veridia(url, **kwargs)
if error["error"] == "insufficient_credits":
notify_credits_exhausted()
raise BusinessError(error)
if error["error"] == "verification_not_found":
raise NotFoundError(error)
if error["error"] == "internal":
logger.error(
"Veridia internal error",
extra={"request_id": error["requestId"], "url": url},
)
raise ExternalServiceError(error)
raise Exception(f"Unhandled Veridia error: {error['error']}")
Best practices
- Switch on
error.error, not on HTTP status. HTTP codes can change for non-breaking reasons; theerrorcode string is part of our public API contract - Always log the
requestId. It's the fastest path to diagnosis if you open a support ticket - Retry with backoff for
internalandrate_limited. Don't retryinvalid_body,unauthorized,verification_not_found, orinsufficient_credits - Surface
validationerrors to your developers, not your end users. End users should see "Something went wrong, please try again" - Monitor
insufficient_creditsas a business alert — it means revenue is being missed
What's next
- Authentication — keys, environments, security
- Rate limits — limits and quota management
- API Reference — back to the API overview