Panorama general
Esta página cubre cómo montar el endpoint y manejar la request. Para el contrato del payload ver Tipos de evento; para las garantías de entrega ver Reintentos.
Por qué webhooks en lugar de polling
| Tema | Polling a GET /v1/verify/{id} | Webhooks |
|---|---|---|
| Latencia entre el veredicto y tu código | Tu intervalo de polling | Una request, apenas existe el veredicto |
| Llamadas a la API por verificación | 3–30 | 1 entrante |
| Clave requerida | Clave secreta, solo del lado del servidor | Ninguna — en su lugar verificás una firma |
| Decisiones de revisión humana | Tenés que seguir haciendo polling indefinidamente | Llegan como un segundo evento |
| Si tu servicio está caído | No perdés nada, pero tenés que seguir haciendo polling | Se reintenta por ~12,6 minutos, después se puede volver a encolar |
La última fila es la que más importa. Un veredicto review es final hasta que una persona lo decida, y eso puede ser horas después. Hacer polling para eso significa o hacer polling para siempre, o rendirte y dejar al usuario colgado. El webhook con la decisión del revisor llega cuando llega.
Configuración
Hay un webhook por tenant, configurado en el panel bajo Settings → Webhook. No hay lista de endpoints, no hay suscripción por evento, y no hay separación test/live para webhooks.
El formulario tiene dos campos:
| Campo | Reglas |
|---|---|
| URL | Tiene que empezar con https://. Si no, se rechaza antes de guardar. También está sujeta a una protección SSRF. |
| Secret | Lo elegís vos. Mínimo 24 caracteres. Dejá el campo en blanco para conservar el secreto actual. |
El secreto no se genera por vos y nunca se muestra de vuelta — el campo es de solo escritura. Generá uno con entropía real y guardalo donde tu aplicación lo lea:
openssl rand -hex 32
Una vez guardado, la próxima verificación que llegue a un veredicto hace POST a tu URL.
Guardar un secreto nuevo reemplaza al viejo al instante. No hay ventana de gracia con doble secreto. Desplegá el secreto nuevo en tu aplicación primero, y recién después guardalo en el panel — ver rotación.
La request
POST /webhooks/veridia HTTP/1.1
Host: yourapp.com
Content-Type: application/json
Veridia-Signature: t=1753142348,v1=4f8a3b9c01ee5d2f...
Veridia-Event: verification.approved
Veridia-Event-Id: evt_9f2c41ab7d8e4c05b6a3e17f2d904c8b
{"id":"evt_9f2c41ab7d8e4c05b6a3e17f2d904c8b","type":"verification.approved", ...}
| Header | Uso |
|---|---|
Veridia-Signature | t=<segundos unix>,v1=<hmac-sha256 en hex>. Verificá esto antes de confiar en nada. |
Veridia-Event | El tipo de evento, espejo del type del cuerpo. Cómodo para routing y métricas; por sí solo no está autenticado — lo que cubre el HMAC es el cuerpo. |
Veridia-Event-Id | El mismo valor que el id del cuerpo. Te permite consultar tu store de deduplicación antes de parsear el cuerpo. |
La firma cubre los bytes crudos del cuerpo. No re-serialices el JSON antes de calcular el HMAC — el orden de las claves y los espacios son parte de lo que se firmó. Ver Verificación de firma.
Respondé 2xx dentro de 10 segundos
El timeout de entrega es de 10 segundos (5 segundos para conectar). Cualquier cosa más lenta cuenta como intento fallido y se reintenta, lo que significa que a tu handler lento-pero-exitoso le van a pedir hacer el mismo trabajo otra vez.
Confirmá primero, trabajá después:
app.post('/webhooks/veridia', express.raw({ type: 'application/json' }), (req, res) => {
// 1. Verificar la firma — rápido, y lo único que debe pasar en línea.
if (!verifyVeridiaSignature(req.header('Veridia-Signature'), req.body, SECRET)) {
return res.status(401).send('invalid signature');
}
// 2. Confirmar. Todo lo que sigue a partir de acá corre por tu cuenta.
res.status(200).send('ok');
// 3. Hacer el trabajo. Los fallos acá son tuyos para reintentar — la entrega
// ya está confirmada y no se va a volver a enviar.
const payload = JSON.parse(req.body.toString('utf8'));
enqueue(payload).catch(err => logger.error({ err, eventId: payload.id }));
});
Ese último comentario es el trade-off que estás aceptando. Confirmar temprano implica que un crash entre el paso 2 y el paso 3 pierde el evento, así que persistí el payload de forma durable (una fila, un mensaje en una cola) como la confirmación, y hacé el procesamiento real a partir de ahí. Los ejemplos siguen todos ese patrón.
Códigos de estado sobre los que actuamos
| Tu respuesta | Qué pasa |
|---|---|
2xx | Entregado. Listo. |
5xx, timeout, error de conexión | Se reintenta según el esquema de backoff |
408, 429 | Se reintenta |
Cualquier otro 4xx | Fallo permanente. No se reintenta — un 401 o un 404 significa que reintentar la request idéntica no puede funcionar. |
Un fallo permanente igual queda registrado en el panel y se puede volver a encolar a mano una vez que arreglaste la causa.
status no es verdict
Esto aplica a la API de polling más que a los webhooks, pero es el error más caro que la API de este producto habilita, y los handlers de webhooks lo heredan cada vez que cruzan datos contra GET /v1/verify/{id}:
statuses el estado del pipeline:queued,processing,completed,failed.verdictes el resultado:approved,review,rejected.
status === "completed" significa que el pipeline corrió, no que la persona pasó. Ramificar sobre eso para dar acceso deja entrar a todos los solicitantes rechazados. Los payloads de webhook no tienen campo status en absoluto — traen type y verdict, y los dos son resultados. Ramificá sobre esos.
Pruebas locales
Los webhooks no pueden llegar a localhost. Esto no es una configuración que se pueda relajar: la URL tiene que ser https://, y la protección SSRF rechaza direcciones de loopback y privadas al momento de enviar. Un túnel es la única forma de recibir entregas reales en desarrollo.
| Herramienta | Notas |
|---|---|
| ngrok | Plan gratuito; la opción de siempre |
| localtunnel | Gratis, open source |
| Cloudflare Tunnel | Gratis; mejor para un entorno de desarrollo de larga vida |
ngrok http 3000
# Forwarding https://abc123.ngrok.io -> http://localhost:3000
# Poné https://abc123.ngrok.io/webhooks/veridia en Settings → Webhook
Para ejercitar el handler sin correr una verificación, reproducí un cuerpo con una firma fresca — ver el script de replay. Firmá exactamente los bytes que mandás.
Checklist
- Verificá
Veridia-Signatureantes de confiar en el cuerpo. Siempre. - Rechazá si
|now - t| > 300segundos. - Respondé
2xxdentro de 10 segundos; persistí primero, procesá después. - Deduplicá por
id(o por el headerVeridia-Event-Id). La entrega es al menos una vez. - Hacé el
switchsobretype. No sobreevent— no existe el campoevent. - Leé
scorescon claves snake_case, y chequeá null enscores.liveness. - Tratá
fieldsExtractedcomo datos personales, también en tus logs. - Logueá
idyverificationIden cada entrega — es lo que te va a pedir soporte.
Qué sigue
- Verificación de firma — el algoritmo, con código
- Tipos de evento — el contrato completo del payload
- Reintentos — backoff, garantías, recuperación
- Ejemplos — implementaciones completas de handlers