Saltar al contenido principal

POST /v1/verify/init

Inicia una verificación nueva. Devuelve un verificationId más tres slots de subida (frente del documento, dorso del documento, selfie) que el cliente usa para subir las imágenes.

POST https://api.xxuxe.online/v1/verify/init

Por qué se pre-crea el ID de verificación

Llamar primero a /init, en lugar de simplemente subir y enviar, hace dos cosas:

  1. Le permite al cliente hilvanar sus propios logs antes de que nada llegue al backend.
  2. Hace que la llamada posterior a /submit sea idempotente — el mismo verificationId en el body del submit siempre significa la misma fila de base de datos.

Autenticación

Bearer token. Sirve cualquiera de las dos familias: publicable (qv_pub_ / qv_pubt_) o secreta (qv_sec_ / qv_sect_).

Authorization: Bearer qv_pub_FJJWXMA2RN2XPRDK6YJX4KTVD0XSQHW9

El tenant se deriva de la clave. No hay campo tenantId.

Body de la request

Todos los campos son opcionales. Podés hacer POST de un body vacío {} y obtener una verificación funcional. country y documentType mejoran de forma significativa la precisión del OCR, así que mandalos cuando los tengas.

CampoTipoDescripción
userRefstringTu propio identificador de usuario, 1-128 caracteres. Se devuelve solo en webhooks
countrystringISO 3166-1 alpha-2, en mayúsculas (PY, BR, MX). Exactamente 2 caracteres
documentTypestringUno de dni, passport, drivers_license, national_id, other
submittedFullNamestringNombre completo tal como lo escribió el usuario, 1-255 caracteres. Alimenta el score name_match
activeLivenessbooleanOpta por el reto de prueba de vida activa. Por defecto false
Los campos desconocidos desaparecen sin error

El esquema es no estricto: las claves que no reconoce se descartan antes de la validación. Mandar tenantId, callbackUrl o metadata acá devuelve 200 OK y el valor simplemente se pierde.

Ninguno de esos tres existe en /init. El tenant viene de tu clave de API; la entrega de webhooks se configura una vez por tenant en el panel, no por request.

userRef — dónde vuelve

userRef se devuelve en los payloads de webhook. No lo devuelve GET /v1/verify/:id, y no está en el evento veridia:complete del widget.

Así que si pensás reconciliar resultados por polling en lugar de por webhook, guardá de tu lado el mapeo verificationId → tu-usuario cuando llamás a /init. Ese es el único vínculo que vas a tener.

activeLiveness

Poner activeLiveness: true agrega un reto emitido por el servidor: la respuesta gana un bloque liveness, y el cliente tiene que capturar y subir una secuencia de frames guiada por balizas que va obteniendo de a una. Es la señal anti-inyección más fuerte disponible, y viene desactivada por defecto.

También cuesta unas 6x el volumen de requests — aproximadamente 26 requests por verificación en lugar de 4. Leé Límites de tasa antes de activarla para tráfico móvil a escala.

El protocolo del reto está implementado por el widget y los SDKs. Si estás construyendo un cliente propio y lo necesitás, hablá con nosotros antes de empezar.

Ejemplo de request

curl

curl -X POST https://api.xxuxe.online/v1/verify/init \
-H "Authorization: Bearer qv_pub_FJJWXMA2RN2XPRDK6YJX4KTVD0XSQHW9" \
-H "Content-Type: application/json" \
-d '{
"userRef": "customer-12345",
"country": "PY",
"documentType": "dni",
"submittedFullName": "Juan Carlos Perez"
}'

JavaScript / Node.js

const response = await fetch('https://api.xxuxe.online/v1/verify/init', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.VERIDIA_PUBLISHABLE_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
userRef: 'customer-12345',
country: 'PY',
documentType: 'dni',
submittedFullName: 'Juan Carlos Perez',
}),
});

const init = await response.json();
console.log(init.verificationId);

Python

import os
import requests

response = requests.post(
"https://api.xxuxe.online/v1/verify/init",
headers={
"Authorization": f"Bearer {os.environ['VERIDIA_PUBLISHABLE_KEY']}",
"Content-Type": "application/json",
},
json={
"userRef": "customer-12345",
"country": "PY",
"documentType": "dni",
"submittedFullName": "Juan Carlos Perez",
},
)
response.raise_for_status()
init = response.json()
print(init["verificationId"])

Respuesta

200 OK

{
"verificationId": "vf_AG07CDWRRFQV4T05ZXG2",
"uploads": {
"docFront": {
"url": "https://api.xxuxe.online/v1/verify/upload/vf_AG07CDWRRFQV4T05ZXG2/doc-front",
"key": "verif/tn_xyz/vf_AG07CDWRRFQV4T05ZXG2/doc-front.jpg",
"method": "PUT",
"headers": {
"X-Veridia-Upload-Token": "5f3c1a9e0b7d4426a1c8ef53d20b9a7c6e14f8b2d9a03c57",
"Content-Type": "image/jpeg"
}
},
"docBack": {
"url": "https://api.xxuxe.online/v1/verify/upload/vf_AG07CDWRRFQV4T05ZXG2/doc-back",
"key": "verif/tn_xyz/vf_AG07CDWRRFQV4T05ZXG2/doc-back.jpg",
"method": "PUT",
"headers": {
"X-Veridia-Upload-Token": "5f3c1a9e0b7d4426a1c8ef53d20b9a7c6e14f8b2d9a03c57",
"Content-Type": "image/jpeg"
}
},
"selfie": {
"url": "https://api.xxuxe.online/v1/verify/upload/vf_AG07CDWRRFQV4T05ZXG2/selfie",
"key": "verif/tn_xyz/vf_AG07CDWRRFQV4T05ZXG2/selfie.jpg",
"method": "PUT",
"headers": {
"X-Veridia-Upload-Token": "5f3c1a9e0b7d4426a1c8ef53d20b9a7c6e14f8b2d9a03c57",
"Content-Type": "image/jpeg"
}
}
},
"expiresAt": 1714604000
}

Los tres slots se devuelven siempre. Usá docBack solo si tu tipo de documento tiene dorso.

Campos de la respuesta

CampoTipoDescripción
verificationIdstring^vf_[A-Za-z0-9]{16,24}$. Pasalo a /submit y a /verify/:id
uploads.docFrontobjectSlot de subida para el frente del documento
uploads.docBackobjectSlot de subida para el dorso del documento
uploads.selfieobjectSlot de subida para la selfie
uploads.*.urlstringDónde hacer PUT de los bytes crudos de la imagen
uploads.*.keystringHandle opaco — devolvelo en /submit
uploads.*.methodstringSiempre "PUT"
uploads.*.headersobjectMandá esto textual. Contiene el token de subida
expiresAtnumberTimestamp Unix en segundos (entero), 15 minutos después del init
livenessobjectPresente solo cuando se mandó activeLiveness: true

expiresAt es una cuenta entera de segundos, no un string ISO 8601. new Date(expiresAt * 1000) en JavaScript; datetime.fromtimestamp(expires_at) en Python.

Subir las imágenes

Esta es la parte que la mayoría de los clientes propios hace mal, así que tiene su propia sección.

PUT https://api.xxuxe.online/v1/verify/upload/{verificationId}/{role}

Las subidas no van a Cloudflare R2, y las URLs de los slots no son URLs presigned de S3. Apuntan al propio Worker de Veridia. El Worker valida el token, verifica que los bytes realmente sean un JPEG dentro de los límites de tamaño, y escribe al almacenamiento en tu nombre, estampando metadata controlada por el servidor que el cliente no puede falsificar.

Esa es una decisión de diseño deliberada, no un detalle de implementación: es el binding cámara→bytes. Ningún byte del cliente llega al almacenamiento sin pasar por esta compuerta, que es el prerrequisito de toda defensa anti-inyección construida encima. También significa que:

  • Tu connect-src de CSP necesita https://api.xxuxe.online. Poner *.r2.cloudflarestorage.com en la lista permitida no hace nada.
  • Las reglas de firewall, el certificate pinning y las listas permitidas de egreso deben apuntar al host de la API.

Autenticación de las subidas

El endpoint de subida no acepta tu clave de API. Mandar Authorization: Bearer ... acá no tiene efecto — el middleware de auth no corre en esta ruta.

Se autentica con X-Veridia-Upload-Token, un token aleatorio de 192 bits que /init generó para esta única verificación y colocó dentro del objeto headers de cada slot. Está atado a la verificación y expira junto con la intención.

La regla práctica: reenviá slot.headers textual. No armes a mano el objeto de headers a partir del Content-Type que ves en el ejemplo — vas a perder el token y toda subida va a fallar con 400 invalid_body, reason: "missing_upload_token", y nunca vas a llegar a /submit.

Requisitos del body

ReglaValorFalla
FormatoJPEG real — debe empezar con los bytes FF D8 FFreason: "not_a_jpeg"
Tamaño mínimo100 bytesreason: "image_too_small"
Tamaño máximo2 MBreason: "image_too_large"

PNG, HEIC, WebP y PDF son todos rechazados. Convertí a JPEG del lado del cliente antes de subir.

Ejemplo funcionando

async function uploadImage(slot, blob) {
const response = await fetch(slot.url, {
method: slot.method, // "PUT"
headers: slot.headers, // textual — lleva X-Veridia-Upload-Token
body: blob, // bytes JPEG crudos
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(`Upload failed: ${response.status} ${err.detail?.reason ?? ''}`);
}
return response.json(); // { ok: true, key: "verif/..." }
}

await uploadImage(init.uploads.docFront, docFrontBlob);
await uploadImage(init.uploads.selfie, selfieBlob);
// docBack solo si el documento tiene dorso

Fijate qué es lo que hace que esto funcione: headers: slot.headers. Todo el resto es accesorio.

Declarar el origen de la captura

Opcionalmente mandá X-Veridia-Capture-Source: camera o upload para registrar de dónde vinieron los píxeles.

Hay una regla que se aplica en vez de solo registrarse: upload es rechazado para el rol selfie y para cualquier frame de prueba de vida, con reason: "upload_source_forbidden_for_biometric". Una foto de galería de una cara no es una selfie, y ningún cliente honesto manda esa combinación. Los documentos sí pueden venir legítimamente de la galería.

El header es autodeclarado y por lo tanto falsificable — es telemetría y triage, no un control de seguridad. No construyas una defensa sobre él, y tampoco lo omitas: el tráfico honesto que se etiqueta a sí mismo mantiene limpio nuestro corpus forense.

Errores de subida

HTTPCódigodetail.reasonCausa
400invalid_bodymissing_upload_tokenNo reenviaste slot.headers
400invalid_bodyinvalid_upload_tokenEl token no corresponde a esta verificación
400invalid_bodykey_mismatchEl rol de la URL no es uno de los que emitió /init
400invalid_bodynot_a_jpegEl body no es un JPEG
400invalid_bodyimage_too_largeMás de 2 MB. Reescalá antes de subir — 2 MB de JPEG son 2-4 MP, de sobra para el OCR
400invalid_bodyimage_too_smallMenos de 100 bytes (normalmente una subida truncada o vacía)
400invalid_bodyupload_source_forbidden_for_biometricOrigen upload en una selfie o un frame de prueba de vida
404verification_not_foundLa intención expiró (1 hora) o nunca existió
429rate_limitedLímite por IP. Ver Límites de tasa

Errores

HTTPCódigo de errorCuándo
400invalid_bodyEl body falló la validación — ver detail.fieldErrors
401missing_api_keySin header Authorization
401invalid_api_keyClave revocada, malformada, o que nunca existió
402insufficient_creditsEl saldo del tenant es cero
403origin_not_allowedRequest de navegador desde un origen que no está en una lista permitida no vacía
429rate_limitedLímite por IP o por tenant
500internal_errorAlgo se rompió de nuestro lado — reportá el requestId

Los códigos son missing_api_key, invalid_api_key e internal_error. No unauthorized, invalid_key ni internal. Catálogo completo: Errores.

Notas

  • Los slots de subida son válidos por 15 minutos (expiresAt). La intención de verificación en sí vive 1 hora — después de eso, /submit y las subidas devuelven verification_not_found.
  • La disposición de almacenamiento es verif/<tenantId>/<verificationId>/<role>.jpg. La key se reconstruye del lado del servidor a partir de la intención validada, nunca a partir de la entrada del cliente.
  • submittedFullName nunca se coloca en una ruta de almacenamiento. Es PII y se queda en el registro de la intención.

Qué sigue

Después de /init, subí las imágenes y luego:

POST /v1/verify/submit →