Skip to main content

POST /v1/verify/submit

Once the client has uploaded the document and selfie, call /submit to run the verification pipeline.

POST https://api.xxuxe.online/v1/verify/submit

This call:

  1. Confirms the submitted keys are the ones /init issued for this verification
  2. Confirms the images actually landed in storage
  3. Runs OCR on the document via Workers AI
  4. Dispatches the job to the ML backend for face match, liveness and verdict
  5. Returns 202 Accepted

Everything after step 5 is asynchronous.

Authentication

Bearer token. Any key belonging to the same tenant as the one used on /init.

Authorization: Bearer qv_pub_FJJWXMA2RN2XPRDK6YJX4KTVD0XSQHW9

The check is on the tenant, not on the key. So a legitimate and often preferable pattern works: start the verification in the browser with your publishable key, then submit from your own server with your secret key. Cross-tenant submission is rejected.

Request body

FieldTypeRequiredDescription
verificationIdstringYesFrom /init. Must match ^vf_[A-Za-z0-9]{16,24}$
keys.docFrontstringYesThe key from init.uploads.docFront. Max 512 chars
keys.selfiestringYesThe key from init.uploads.selfie. Max 512 chars
keys.docBackstringNoThe key from init.uploads.docBack, if you captured it
livenessScorenumberNoClient-side liveness score, 0-100. Used as an additional soft signal
metadataobjectNoAccepted by the schema. See the warning below

keys is mandatory, and it is not decorative

keys is what ties the stored bytes to this verification. There is no way to submit without it.

The values must match exactly what /init returned. Take them from the init response rather than reconstructing the strings — a mismatch is rejected with doc_front_key_mismatch, selfie_key_mismatch or doc_back_key_mismatch.

metadata is accepted and then discarded

The schema validates metadata, and the request returns 202. But the field is not forwarded to the backend and it is not echoed in webhooks. It stops at the edge of the Worker.

This is the worst kind of failure — it takes your data, succeeds, and drops it. If you were planning to route on a campaign ID, an A/B bucket or a brand tag, that will not work.

Use userRef on /init instead. It is persisted, and it is the one field echoed in webhook payloads.

Example request

curl

curl -X POST https://api.xxuxe.online/v1/verify/submit \
-H "Authorization: Bearer qv_pub_FJJWXMA2RN2XPRDK6YJX4KTVD0XSQHW9" \
-H "Content-Type: application/json" \
-d '{
"verificationId": "vf_AG07CDWRRFQV4T05ZXG2",
"keys": {
"docFront": "verif/tn_xyz/vf_AG07CDWRRFQV4T05ZXG2/doc-front.jpg",
"selfie": "verif/tn_xyz/vf_AG07CDWRRFQV4T05ZXG2/selfie.jpg"
},
"livenessScore": 92.5
}'

JavaScript / Node.js

const response = await fetch('https://api.xxuxe.online/v1/verify/submit', {
method: 'POST',
headers: {
'Authorization': `Bearer ${publishableKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
verificationId: init.verificationId,
keys: {
docFront: init.uploads.docFront.key,
selfie: init.uploads.selfie.key,
// docBack: init.uploads.docBack.key, // only if you captured it
},
livenessScore: 92.5,
}),
});

const data = await response.json(); // 202 Accepted
console.log('Submitted. Poll:', data.statusUrl);

Python

import os
import requests

response = requests.post(
"https://api.xxuxe.online/v1/verify/submit",
headers={
"Authorization": f"Bearer {os.environ['VERIDIA_PUBLISHABLE_KEY']}",
"Content-Type": "application/json",
},
json={
"verificationId": init["verificationId"],
"keys": {
"docFront": init["uploads"]["docFront"]["key"],
"selfie": init["uploads"]["selfie"]["key"],
},
"livenessScore": 92.5,
},
)
response.raise_for_status()
print("Poll:", response.json()["statusUrl"])

Response

202 Accepted

{
"verificationId": "vf_AG07CDWRRFQV4T05ZXG2",
"status": "queued",
"statusUrl": "https://api.xxuxe.online/v1/verify/vf_AG07CDWRRFQV4T05ZXG2"
}
FieldTypeDescription
verificationIdstringSame ID you submitted
statusstringqueued, processing, or completed
statusUrlstringWhere to poll for the verdict
status here is not a verdict

status: "completed" on this response — or on any later poll — means the pipeline finished. It does not mean the person passed. The outcome lives in a separate field, verdict, and it is not in this response at all.

Reading the verdict requires a secret key on GET /v1/verify/:id. The publishable key that made this call cannot fetch it.

Idempotency

Calling /submit twice with the same verificationId is safe.

The backend deduplicates: if the verification is already processing or completed, the duplicate is ignored and you get the current status back rather than a second pipeline run. A verification still queued or failed may be re-enqueued.

The OCR result is cached for 30 minutes, so a retry does not re-hit Workers AI either.

So if your client gets a network error after sending /submit, retry with the same body. You will not create a duplicate verification.

On billing

Authentication rejects requests when the tenant's credit balance is zero (insufficient_credits, 402), but this endpoint does not currently decrement that balance per verification. Do not build usage forecasting or a consumption meter on the assumption that one submit equals one credit off the counter — reconcile against your own records instead.

Timing

The 202 typically comes back in a couple of seconds; the OCR call dominates. The verdict itself lands a few seconds after that, asynchronously.

Rather than polling for it, use webhooks. If you must poll, GET /v1/verify/:id has the pattern — and note that polling from a single server hits the per-IP rate limit long before the per-tenant one.

Errors

HTTPError codedetail.reasonWhen
400invalid_bodyBody failed validation. See detail.fieldErrors
400invalid_bodydoc_front_key_mismatchkeys.docFront isn't what /init returned
400invalid_bodyselfie_key_mismatchkeys.selfie isn't what /init returned
400invalid_bodydoc_back_key_mismatchkeys.docBack isn't what /init returned
400invalid_bodydoc_front_not_uploadedNo object at that key — the client never uploaded
400invalid_bodyselfie_not_uploadedSame, for the selfie
400invalid_bodydoc_front_disappearedObject existed at check time but was gone before OCR (very rare)
401missing_api_key / invalid_api_keySee Authentication
402insufficient_creditsTenant balance is zero
404verification_not_foundNever created via /init, expired after 1 hour, or belongs to another tenant
429rate_limited30/min per tenant, or the per-IP limit
500internal_errorReport the requestId
503backend_unavailablePipeline unreachable. Retry with backoff

insufficient_credits is 402, not 403. The internal error code is internal_error, not internal. Full catalog: Errors.

If you get doc_front_not_uploaded while believing you uploaded, the usual cause is an upload that failed with missing_upload_token because the client rebuilt the headers instead of forwarding slot.headers verbatim. See Uploading the images.

Notes

  • The verification must have been created within the last hour. The intent expires after that and you get verification_not_found.
  • livenessScore is optional. It is a soft signal that nudges the weighted confidence; it never decides the outcome on its own.
  • Capture provenance is read from server-stamped metadata written when the bytes arrived, not from this request body. /submit cannot rewrite it.

What's next

GET /v1/verify/:id — fetch the verdict