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_body—Body 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_key—See Authentication
402insufficient_credits—Tenant balance is zero
404verification_not_found—Never created via /init, expired after 1 hour, or belongs to another tenant
429rate_limited—30/min per tenant, or the per-IP limit
500internal_error—Report the requestId
503backend_unavailable—Pipeline 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