Saltar al contenido principal

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:

  1. Verifica que todas las imagenes subidas hayan llegado realmente a R2
  2. Corre OCR sobre el documento via Workers AI
  3. Debita un credito del balance del tenant
  4. Despacha el job al backend ML para face match + liveness + veredicto
  5. Devuelve 202 Accepted con 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

CampoTipoRequeridoDescripcion
verificationIdstringSiEl ID devuelto por /init. Formato: vf_[A-Za-z0-9]{16,24}
keys.docFrontstringSiLa R2 key devuelta por /init para el frente del documento. Max 512 chars
keys.selfiestringSiLa R2 key devuelta por /init para la selfie. Max 512 chars
keys.docBackstringNoLa R2 key para el reverso, si tu flow lo capturo
livenessScorenumberNoScore de liveness opcional desde MediaPipe (0-100). Usado como senal adicional
metadataobjectNoPares 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

CampoTipoDescripcion
verificationIdstringEl mismo ID que enviaste — confirma que la request fue aceptada
statusstringEstado actual: queued, processing, o completed
statusUrlstringDonde 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):

PasoLatenciaQue
1<50msValida el body de la request (Zod schema)
2<100msBusca el intent desde el cache KV
3<200msVerifica que los objetos R2 existan via llamadas HEAD
4<2sCorre OCR via Workers AI (cacheado en retries)
5<200msDespacha al backend ML, devuelve 202
6asyncBackend corre face match + liveness + veredicto
7asyncBackend persiste a MySQL + dispara webhook

Tiempo total desde /submit al veredicto: tipicamente 2-3 segundos.

Errores

HTTPError codeCuando
400invalid_bodyEl body fallo validacion. Mira detail.fieldErrors
400invalid_body (con reason: "doc_front_key_mismatch")El keys.docFront no coincide con lo que /init devolvio
400invalid_body (con reason: "selfie_key_mismatch")El keys.selfie no coincide con lo que /init devolvio
400invalid_body (con reason: "doc_front_not_uploaded")El objeto R2 no existe — el cliente nunca lo subio
400invalid_body (con reason: "selfie_not_uploaded")Igual, para la selfie
404verification_not_foundEl verificationId nunca fue creado via /init, o el intent expiro (>1h)
401unauthorized / invalid_keyIssue de auth. Mira Autenticacion
403insufficient_creditsEl tenant tiene 0 creditos
429rate_limitedHit el rate limit de /submit (30 req/min por defecto)
500internalAlgo se rompio. Incluye el requestId en tu ticket de soporte

Catalogo completo: Errores.

Notas

  • El verificationId debe haber sido creado en la ultima hora. Despues de eso, el intent expira y obtenes verification_not_found
  • La misma API key usada en /init debe ser usada aca. Submission cross-tenant es rechazada
  • El livenessScore es opcional pero recomendado — incluso un score basico browser-side mejora el accuracy del veredicto
  • metadata es 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:

GET /v1/verify/:id