POST /v1/verify/submit
After your client has uploaded the document and selfie images to R2 (using the presigned URLs from /init), call /submit to trigger the verification pipeline.
POST https://api.xxuxe.online/v1/verify/submit
This is the call that:
- Verifies all uploaded images actually landed in R2
- Runs OCR on the document via Workers AI
- Debits one credit from the tenant's balance
- Dispatches the job to the ML backend for face match + liveness + verdict
- Returns
202 Acceptedwith a status URL to poll
Authentication
Bearer token. Same key used in /init (the tenant must match).
Authorization: Bearer qv_pub_FJJWXMA2RN2XPRDK6YJX4KTVD0XSQHW9
Request body
| Field | Type | Required | Description |
|---|---|---|---|
verificationId | string | Yes | The ID returned by /init. Format: vf_[A-Za-z0-9]{16,24} |
keys.docFront | string | Yes | The R2 key returned by /init for the front of document. Max 512 chars |
keys.selfie | string | Yes | The R2 key returned by /init for the selfie. Max 512 chars |
keys.docBack | string | No | The R2 key for the back of document, if your flow captured it |
livenessScore | number | No | Optional liveness score from MediaPipe (0-100). Used as additional signal |
metadata | object | No | Optional key-value pairs for your bookkeeping. Echoed in webhooks |
The keys.* values must match exactly what /init returned. Submitting arbitrary keys is rejected (doc_front_key_mismatch, selfie_key_mismatch, doc_back_key_mismatch).
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,
"metadata": {
"campaign": "spring_2026",
"platform": "web"
}
}'
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: initResponse.verificationId,
keys: {
docFront: initResponse.uploads.docFront.key,
selfie: initResponse.uploads.selfie.key,
},
livenessScore: 92.5,
}),
});
if (response.status === 202) {
const data = await response.json();
console.log('Submitted, poll at:', 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_response["verificationId"],
"keys": {
"docFront": init_response["uploads"]["docFront"]["key"],
"selfie": init_response["uploads"]["selfie"]["key"],
},
"livenessScore": 92.5,
},
)
response.raise_for_status()
data = response.json()
print("Status URL:", data["statusUrl"])
PHP
<?php
$payload = json_encode([
"verificationId" => $initResponse['verificationId'],
"keys" => [
"docFront" => $initResponse['uploads']['docFront']['key'],
"selfie" => $initResponse['uploads']['selfie']['key'],
],
"livenessScore" => 92.5,
]);
$ch = curl_init("https://api.xxuxe.online/v1/verify/submit");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $_ENV['VERIDIA_PUBLISHABLE_KEY'],
"Content-Type: application/json",
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
echo "Status URL: " . $data['statusUrl'];
Response
202 Accepted
{
"verificationId": "vf_AG07CDWRRFQV4T05ZXG2",
"status": "queued",
"statusUrl": "https://api.xxuxe.online/v1/verify/vf_AG07CDWRRFQV4T05ZXG2"
}
Response fields
| Field | Type | Description |
|---|---|---|
verificationId | string | Same ID you submitted — confirms the request was accepted |
status | string | Current state: queued, processing, or completed |
statusUrl | string | Where to poll for the verdict (typically /v1/verify/:id) |
The verification is now in the pipeline. The verdict typically comes back in 2-3 seconds. Use the statusUrl to fetch it.
Idempotency
Calling /submit twice with the same verificationId is safe. The backend deduplicates by verificationId, so you'll get the same result both times. The OCR result is cached for 30 minutes — retries don't re-trigger Workers AI calls.
This means: if your client gets a network error after sending /submit, retry with the same body. You won't double-bill or create duplicate verifications.
Pipeline behind the scenes
When /submit is called, here's what happens (the API returns immediately at step 5 — the rest is async):
| Step | Latency | What |
|---|---|---|
| 1 | <50ms | Validate request body (Zod schema) |
| 2 | <100ms | Look up intent from KV cache |
| 3 | <200ms | Verify R2 objects exist via HEAD calls |
| 4 | <2s | Run OCR via Workers AI (cached on retry) |
| 5 | <200ms | Dispatch to ML backend, return 202 |
| 6 | async | Backend runs face match + liveness + verdict |
| 7 | async | Backend persists to MySQL + fires webhook |
Total time from /submit to verdict: typically 2-3 seconds.
Errors
| HTTP | Error code | When |
|---|---|---|
400 | invalid_body | Request body failed validation. See detail.fieldErrors |
400 | invalid_body (with reason: "doc_front_key_mismatch") | The keys.docFront doesn't match what /init returned |
400 | invalid_body (with reason: "selfie_key_mismatch") | The keys.selfie doesn't match what /init returned |
400 | invalid_body (with reason: "doc_front_not_uploaded") | The R2 object doesn't exist — client never uploaded it |
400 | invalid_body (with reason: "selfie_not_uploaded") | Same, for the selfie |
404 | verification_not_found | The verificationId was never created via /init, or the intent expired (>1h) |
401 | unauthorized / invalid_key | Auth issue. See Authentication |
403 | insufficient_credits | Tenant has 0 credits |
429 | rate_limited | Hit the /submit rate limit (30 req/min by default) |
500 | internal | Something broke. Include the requestId in your support ticket |
Full catalog: Errors.
Notes
- The
verificationIdmust have been created within the last hour. After that, the intent expires and you getverification_not_found - The same API key used in
/initmust be used here. Cross-tenant submission is rejected - The
livenessScoreis optional but recommended — even a basic browser-side score improves verdict accuracy metadatais opaque to Veridia — it's just echoed in webhooks. Use it for things like A/B test buckets, campaign IDs, or platform tags
What's next
Now that the verification is in the pipeline, fetch the verdict: