Skip to main content

API Reference

Veridia exposes a small REST API. JSON in, JSON out. No SOAP, no GraphQL, no XML.

Base URL

https://api.xxuxe.online

All requests must use HTTPS.

Endpoints

MethodPathAuthPurpose
POST/v1/verify/initAPI keyStart a verification, get upload slots
PUT/v1/verify/upload/:verificationId/:roleUpload tokenUpload one image
GET/v1/verify/challenge/:verificationId/nextUpload tokenNext active-liveness beacon (opt-in flows only)
POST/v1/verify/submitAPI keyRun the pipeline on the uploaded images
GET/v1/verify/:idAPI key — secret onlyFetch status and verdict
GET/healthNoneLiveness check

The widget uses all of these under the hood. You call them directly when you are building a server-side flow or a custom mobile client.

Two of them are easy to miss, and both are load-bearing:

  • PUT /v1/verify/upload/... is where every image byte in the product actually goes. It does not take a bearer token. See POST /v1/verify/init.
  • GET /v1/verify/challenge/.../next only exists in flows that opted into active liveness with activeLiveness: true on /init.

There is no /v1/verifications and no /v1/verifications/:id. Those paths return 404 not_found; the results endpoint is GET /v1/verify/:id.

Authentication

Most endpoints take a bearer token:

Authorization: Bearer qv_pub_FJJWXMA2RN2XPRDK6YJX4KTVD0XSQHW9

Two key families exist, and the split is the most important thing on this page:

FamilyLive prefixTest prefixWhere it runsCan it read verdicts?
Publishableqv_pub_qv_pubt_Browser, widget, mobile appNo
Secretqv_sec_qv_sect_Your server onlyYes

A publishable key can start a verification and submit it. It cannot read the outcome — GET /v1/verify/:id rejects it with 401 secret_key_required. That is deliberate: a publishable key sits in your page source where anyone can read it, so it must never be able to fetch a KYC verdict.

Note the test prefixes: qv_pubt_ and qv_sect_. Not qv_pub_test_.

The upload and challenge endpoints use neither. They authenticate with the short-lived X-Veridia-Upload-Token that /init returns inside each upload slot's headers.

Full details: Authentication.

Versioning

The API is versioned in the URL path: /v1/.... Breaking changes get a new version path (/v2/...).

Non-breaking additions — new optional request fields, new response fields, new endpoints — happen on /v1 without notice. Write clients that ignore response fields they do not recognise.

Request format

All POST bodies are JSON:

POST /v1/verify/init HTTP/1.1
Host: api.xxuxe.online
Authorization: Bearer qv_pub_...
Content-Type: application/json

{
"userRef": "customer-12345",
"country": "PY",
"documentType": "dni"
}
Unknown fields are discarded silently

Request bodies are validated with a non-strict schema. A key we do not recognise is dropped without an error — you get 200 OK and the value is simply gone.

So if you invent a field (tenantId, callbackUrl, metadata on /init) nothing tells you it did not take effect. Check the field tables on each endpoint page rather than assuming a field worked because the request succeeded.

Response format

Every response carries a requestId, and the same value is in the X-Request-Id response header — so you can correlate even when JSON parsing fails. Log it. It is the fastest path to a diagnosis on a support ticket.

{
"verificationId": "vf_AG07CDWRRFQV4T05ZXG2",
"uploads": { "docFront": { "...": "..." } },
"expiresAt": 1714604000
}

Errors use a consistent shape:

{
"error": "invalid_body",
"message": "Request body failed validation",
"requestId": "9f511d92ac11236d-SJC",
"detail": {
"fieldErrors": {
"country": ["expected 2 characters"]
}
}
}

Switch on error, not on the HTTP status. See Errors for the full catalog.

Status is not verdict

The single most expensive mistake this API allows:

FieldAxisValues
statusDid the pipeline run?queued, processing, completed, failed
verdictDid the person pass?approved, review, rejected

status: "completed" means the pipeline finished. It says nothing about whether the applicant was accepted. Branching on status to grant an account admits every rejected applicant, silently, with no error anywhere.

// WRONG — this onboards everyone the system rejected
if (result.status === 'completed') enableAccount(userId);

// RIGHT
if (result.status === 'completed' && result.verdict === 'approved') enableAccount(userId);

verdict is absent (or null) until status is completed. Details on GET /v1/verify/:id.

Rate limits

Two layers. The one that catches people out is the per-IP layer, which is checked before your key is read:

LayerLimit
Per client IP, all routes100 requests / 60 s
Per tenant — /v1/verify/init60 / 60 s
Per tenant — /v1/verify/submit30 / 60 s
Per tenant — /v1/verify/:id600 / 60 s

A verification with active liveness makes about 26 requests from the user's device, so a handful of mobile users behind one CGNAT address can exhaust the per-IP budget while your tenant counters look idle. The full explanation, and what to do about it, is on Rate limits — read it before shipping to mobile.

429 responses carry a Retry-After header.

CORS

Preflight (OPTIONS) responses are cached for 10 minutes.

Cross-origin requests are not restricted by default. A publishable key can carry an allowed-origins list, but every key is created with that list empty, and an empty list allows every origin. Treat it as a way to scope where your widget runs once you populate it — not as an access control. See the warning in Authentication.

Where to next