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
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /v1/verify/init | API key | Start a verification, get upload slots |
PUT | /v1/verify/upload/:verificationId/:role | Upload token | Upload one image |
GET | /v1/verify/challenge/:verificationId/next | Upload token | Next active-liveness beacon (opt-in flows only) |
POST | /v1/verify/submit | API key | Run the pipeline on the uploaded images |
GET | /v1/verify/:id | API key — secret only | Fetch status and verdict |
GET | /health | None | Liveness 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/.../nextonly exists in flows that opted into active liveness withactiveLiveness: trueon/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:
| Family | Live prefix | Test prefix | Where it runs | Can it read verdicts? |
|---|---|---|---|---|
| Publishable | qv_pub_ | qv_pubt_ | Browser, widget, mobile app | No |
| Secret | qv_sec_ | qv_sect_ | Your server only | Yes |
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"
}
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:
| Field | Axis | Values |
|---|---|---|
status | Did the pipeline run? | queued, processing, completed, failed |
verdict | Did 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:
| Layer | Limit |
|---|---|
| Per client IP, all routes | 100 requests / 60 s |
Per tenant — /v1/verify/init | 60 / 60 s |
Per tenant — /v1/verify/submit | 30 / 60 s |
Per tenant — /v1/verify/:id | 600 / 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
- Authentication — key families, environments, what the origin list does and does not do
- POST /v1/verify/init — start a verification, and how uploads actually work
- POST /v1/verify/submit — run the pipeline
- GET /v1/verify/:id — fetch the verdict
- Errors — error code reference
- Rate limits — both limits, and the CGNAT caveat