POST /v1/verify/submit
Despues que tu cliente haya subido las imagenes del documento y la selfie a R2 (usando las URLs presigned de /init), llama a /submit para disparar el pipeline de verificacion.
POST https://api.xxuxe.online/v1/verify/submit
Esta es la llamada que:
- Verifica que todas las imagenes subidas hayan llegado realmente a R2
- Corre OCR sobre el documento via Workers AI
- Debita un credito del balance del tenant
- Despacha el job al backend ML para face match + liveness + veredicto
- Devuelve
202 Acceptedcon una status URL para hacer polling
Autenticacion
Bearer token. La misma key usada en /init (el tenant tiene que coincidir).
Authorization: Bearer qv_pub_FJJWXMA2RN2XPRDK6YJX4KTVD0XSQHW9
Request body
| Campo | Tipo | Requerido | Descripcion |
|---|---|---|---|
verificationId | string | Si | El ID devuelto por /init. Formato: vf_[A-Za-z0-9]{16,24} |
keys.docFront | string | Si | La R2 key devuelta por /init para el frente del documento. Max 512 chars |
keys.selfie | string | Si | La R2 key devuelta por /init para la selfie. Max 512 chars |
keys.docBack | string | No | La R2 key para el reverso, si tu flow lo capturo |
livenessScore | number | No | Score de liveness opcional desde MediaPipe (0-100). Usado como senal adicional |
metadata | object | No | Pares key-value opcionales para tu bookkeeping. Eco en webhooks |
Los valores de keys.* deben coincidir exactamente con lo que devolvio /init. Submitear keys arbitrarias es rechazado (doc_front_key_mismatch, selfie_key_mismatch, doc_back_key_mismatch).
Request de ejemplo
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, polling en:', 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'];
Respuesta
202 Accepted
{
"verificationId": "vf_AG07CDWRRFQV4T05ZXG2",
"status": "queued",
"statusUrl": "https://api.xxuxe.online/v1/verify/vf_AG07CDWRRFQV4T05ZXG2"
}
Campos de respuesta
| Campo | Tipo | Descripcion |
|---|---|---|
verificationId | string | El mismo ID que enviaste — confirma que la request fue aceptada |
status | string | Estado actual: queued, processing, o completed |
statusUrl | string | Donde hacer polling para el veredicto (tipicamente /v1/verify/:id) |
La verificacion ahora esta en el pipeline. El veredicto tipicamente vuelve en 2-3 segundos. Usa la statusUrl para obtenerlo.
Idempotencia
Llamar a /submit dos veces con el mismo verificationId es seguro. El backend deduplica por verificationId, asi obtenes el mismo resultado ambas veces. El resultado del OCR esta cacheado por 30 minutos — los retries no re-disparan llamadas a Workers AI.
Esto significa: si tu cliente recibe un error de red despues de mandar /submit, reintenta con el mismo body. No vas a doble-facturarte ni crear verificaciones duplicadas.
Pipeline detras de escena
Cuando /submit es llamado, esto es lo que pasa (la API devuelve inmediatamente en el paso 5 — el resto es async):
| Paso | Latencia | Que |
|---|---|---|
| 1 | <50ms | Valida el body de la request (Zod schema) |
| 2 | <100ms | Busca el intent desde el cache KV |
| 3 | <200ms | Verifica que los objetos R2 existan via llamadas HEAD |
| 4 | <2s | Corre OCR via Workers AI (cacheado en retries) |
| 5 | <200ms | Despacha al backend ML, devuelve 202 |
| 6 | async | Backend corre face match + liveness + veredicto |
| 7 | async | Backend persiste a MySQL + dispara webhook |
Tiempo total desde /submit al veredicto: tipicamente 2-3 segundos.
Errores
| HTTP | Error code | Cuando |
|---|---|---|
400 | invalid_body | El body fallo validacion. Mira detail.fieldErrors |
400 | invalid_body (con reason: "doc_front_key_mismatch") | El keys.docFront no coincide con lo que /init devolvio |
400 | invalid_body (con reason: "selfie_key_mismatch") | El keys.selfie no coincide con lo que /init devolvio |
400 | invalid_body (con reason: "doc_front_not_uploaded") | El objeto R2 no existe — el cliente nunca lo subio |
400 | invalid_body (con reason: "selfie_not_uploaded") | Igual, para la selfie |
404 | verification_not_found | El verificationId nunca fue creado via /init, o el intent expiro (>1h) |
401 | unauthorized / invalid_key | Issue de auth. Mira Autenticacion |
403 | insufficient_credits | El tenant tiene 0 creditos |
429 | rate_limited | Hit el rate limit de /submit (30 req/min por defecto) |
500 | internal | Algo se rompio. Incluye el requestId en tu ticket de soporte |
Catalogo completo: Errores.
Notas
- El
verificationIddebe haber sido creado en la ultima hora. Despues de eso, el intent expira y obtenesverification_not_found - La misma API key usada en
/initdebe ser usada aca. Submission cross-tenant es rechazada - El
livenessScorees opcional pero recomendado — incluso un score basico browser-side mejora el accuracy del veredicto metadataes opaco para Veridia — solo se echo en webhooks. Usalo para cosas como buckets de A/B test, campaign IDs, o tags de plataforma
Que sigue
Ahora que la verificacion esta en el pipeline, obtene el veredicto: