Skip to main content

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"]
}
}
}
FieldTypeDescription
errorstringMachine-readable error code — switch on this in your code
messagestringHuman-readable summary, mostly for logs
requestIdstringUnique ID for this request — include it in support tickets
detailobjectOptional. 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_body400 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 > 128 chars)
  • Invalid enum value (e.g., documentType: "id" instead of "dni")
  • verificationId doesn't match ^vf_[A-Za-z0-9]{16,24}$

On /submit specifically, detail.reason may be:

ReasonMeaning
doc_front_key_mismatchThe keys.docFront doesn't match what /init returned
selfie_key_mismatchThe keys.selfie doesn't match what /init returned
doc_back_key_mismatchThe keys.docBack doesn't match what /init returned
doc_front_not_uploadedThe R2 object for the front doesn't exist — client never uploaded
selfie_not_uploadedThe R2 object for the selfie doesn't exist
doc_front_disappearedThe R2 object existed during HEAD but was gone before OCR (extremely rare)

Recovery: Fix the request and retry. These are client errors.

unauthorized401 Unauthorized

The Authorization header is missing, malformed, or doesn't use the Bearer scheme.

Recovery: Add a valid Authorization: Bearer <key> header.

invalid_key401 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_allowed403 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_environment403 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_credits403 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_found404 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_found404 Not Found

The route doesn't exist.

Recovery: Check the URL. Common typos: /v1/verifications instead of /v1/verify/:id.

rate_limited429 Too Many Requests

Hit the per-tenant rate limit. The Retry-After response header tells you how long to wait (in seconds).

EndpointDefault limit
/v1/verify/init60 / minute
/v1/verify/submit30 / minute
/v1/verify/:id600 / minute

Recovery:

  • Wait for the duration in Retry-After, then retry
  • Add throttling on your side
  • Contact support to lift limits per-tenant

internal500 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 requestId from 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; the error code 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 internal and rate_limited. Don't retry invalid_body, unauthorized, verification_not_found, or insufficient_credits
  • Surface validation errors to your developers, not your end users. End users should see "Something went wrong, please try again"
  • Monitor insufficient_credits as a business alert — it means revenue is being missed

What's next