POST /v1/verify/submit
Una vez que el cliente subió el documento y la selfie, llamá a /submit para correr el pipeline de verificación.
POST https://api.xxuxe.online/v1/verify/submit
Esta llamada:
- Confirma que las keys enviadas son las que
/initemitió para esta verificación - Confirma que las imágenes realmente llegaron al almacenamiento
- Corre OCR sobre el documento vía Workers AI
- Despacha el trabajo al backend de ML para coincidencia facial, prueba de vida y veredicto
- Devuelve
202 Accepted
Todo lo que sigue al paso 5 es asincrónico.
Autenticación
Bearer token. Cualquier clave que pertenezca al mismo tenant que la usada en /init.
Authorization: Bearer qv_pub_FJJWXMA2RN2XPRDK6YJX4KTVD0XSQHW9
La verificación es sobre el tenant, no sobre la clave. Así que funciona un patrón legítimo y a menudo preferible: iniciás la verificación en el navegador con tu clave publicable, y después enviás desde tu propio servidor con tu clave secreta. El envío entre tenants distintos se rechaza.
Body de la request
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
verificationId | string | Sí | Viene de /init. Debe coincidir con ^vf_[A-Za-z0-9]{16,24}$ |
keys.docFront | string | Sí | La key de init.uploads.docFront. Máx. 512 caracteres |
keys.selfie | string | Sí | La key de init.uploads.selfie. Máx. 512 caracteres |
keys.docBack | string | No | La key de init.uploads.docBack, si la capturaste |
livenessScore | number | No | Score de prueba de vida del lado del cliente, 0-100. Se usa como señal blanda adicional |
metadata | object | No | Aceptado por el esquema. Ver la advertencia de abajo |
keys es obligatorio, y no es decorativo
keys es lo que ata los bytes almacenados a esta verificación. No hay forma de enviar sin él.
Los valores deben coincidir exactamente con lo que devolvió /init. Tomalos de la respuesta del init en lugar de reconstruir los strings — una discrepancia se rechaza con doc_front_key_mismatch, selfie_key_mismatch o doc_back_key_mismatch.
metadata se acepta y después se descartaEl esquema valida metadata, y la request devuelve 202. Pero el campo no se reenvía al backend y no se devuelve en los webhooks. Se queda en el borde del Worker.
Este es el peor tipo de falla — toma tus datos, tiene éxito, y los tira. Si pensabas rutear en base a un ID de campaña, un bucket de A/B o una etiqueta de marca, eso no va a funcionar.
Usá userRef en /init en su lugar. Ese sí se persiste, y es el único campo que se devuelve en los payloads de webhook.
Ejemplo de 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, // solo si la capturaste
},
livenessScore: 92.5,
}),
});
const data = await response.json(); // 202 Accepted
console.log('Enviado. Consultar:', 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("Consultar:", response.json()["statusUrl"])
Respuesta
202 Accepted
{
"verificationId": "vf_AG07CDWRRFQV4T05ZXG2",
"status": "queued",
"statusUrl": "https://api.xxuxe.online/v1/verify/vf_AG07CDWRRFQV4T05ZXG2"
}
| Campo | Tipo | Descripción |
|---|---|---|
verificationId | string | El mismo ID que enviaste |
status | string | queued, processing, o completed |
statusUrl | string | Dónde hacer polling para obtener el veredicto |
status no es un veredictostatus: "completed" en esta respuesta — o en cualquier consulta posterior — significa que el pipeline terminó. No significa que la persona pasó. El resultado vive en un campo separado, verdict, y no está en esta respuesta en absoluto.
Leer el veredicto requiere una clave secreta en GET /v1/verify/:id. La clave publicable que hizo esta llamada no puede obtenerlo.
Idempotencia
Llamar a /submit dos veces con el mismo verificationId es seguro.
El backend deduplica: si la verificación ya está en processing o completed, el duplicado se ignora y recibís el status actual en lugar de una segunda corrida del pipeline. Una verificación que sigue en queued o failed puede volver a encolarse.
El resultado del OCR se cachea por 30 minutos, así que un reintento tampoco vuelve a golpear a Workers AI.
Entonces, si tu cliente recibe un error de red después de mandar /submit, reintentá con el mismo body. No vas a crear una verificación duplicada.
La autenticación rechaza requests cuando el saldo de créditos del tenant es cero (insufficient_credits, 402), pero este endpoint actualmente no descuenta ese saldo por verificación. No construyas proyecciones de uso ni un medidor de consumo asumiendo que un submit equivale a un crédito menos en el contador — reconciliá contra tus propios registros.
Tiempos
El 202 típicamente vuelve en un par de segundos; la llamada de OCR domina. El veredicto en sí llega unos segundos después, de forma asincrónica.
En lugar de hacer polling para obtenerlo, usá webhooks. Si tenés que hacer polling, GET /v1/verify/:id tiene el patrón — y tené en cuenta que hacer polling desde un solo servidor toca el límite de tasa por IP mucho antes que el de por tenant.
Errores
| HTTP | Código de error | detail.reason | Cuándo |
|---|---|---|---|
400 | invalid_body | — | El body falló la validación. Ver detail.fieldErrors |
400 | invalid_body | doc_front_key_mismatch | keys.docFront no es lo que devolvió /init |
400 | invalid_body | selfie_key_mismatch | keys.selfie no es lo que devolvió /init |
400 | invalid_body | doc_back_key_mismatch | keys.docBack no es lo que devolvió /init |
400 | invalid_body | doc_front_not_uploaded | No hay objeto en esa key — el cliente nunca subió |
400 | invalid_body | selfie_not_uploaded | Lo mismo, para la selfie |
400 | invalid_body | doc_front_disappeared | El objeto existía al momento de la verificación pero desapareció antes del OCR (muy raro) |
401 | missing_api_key / invalid_api_key | — | Ver Autenticación |
402 | insufficient_credits | — | El saldo del tenant es cero |
404 | verification_not_found | — | Nunca creada vía /init, expirada después de 1 hora, o pertenece a otro tenant |
429 | rate_limited | — | 30/min por tenant, o el límite por IP |
500 | internal_error | — | Reportá el requestId |
503 | backend_unavailable | — | Pipeline inalcanzable. Reintentá con backoff |
insufficient_credits es 402, no 403. El código de error interno es internal_error, no internal. Catálogo completo: Errores.
Si recibís doc_front_not_uploaded creyendo que subiste la imagen, la causa habitual es una subida que falló con missing_upload_token porque el cliente rearmó los headers en lugar de reenviar slot.headers textual. Ver Subir las imágenes.
Notas
- La verificación tiene que haber sido creada dentro de la última hora. La intención expira después de eso y recibís
verification_not_found. livenessScorees opcional. Es una señal blanda que empuja levemente la confianza ponderada; nunca decide el resultado por sí sola.- La procedencia de la captura se lee de la metadata estampada por el servidor cuando llegaron los bytes, no del body de esta request.
/submitno puede reescribirla.
Qué sigue
GET /v1/verify/:id → — obtener el veredicto