Webhook de la Verificación de Identidad
Cada decisión de una Verificación de Identidad, automática o tomada por tu equipo en el dashboard, genera un POST a la URL que registraste. El evento no trae datos personales: solo IDs, la decisión y los motivos.
Configurar
En el dashboard, en Verificações > Configurações > Integrações:
- Registra la URL de tu endpoint. Solo
httpsy solo direcciones públicas. - Copia el secreto y guárdalo en una variable de entorno (ej.:
CPFHUB_WEBHOOK_SECRET). - Usa el envío de prueba para recibir un evento
kyc.testy comprobar la firma.
Evento
{
"event": "kyc.decision",
"eventId": "3e8a1f20-5c4b-4d9e-8f7a-6b2c1d0e9f8a",
"verificationId": "8f3c2a1e-77b0-4d3c-9a1f-0e5b6c7d8e9f",
"linkId": "5b1d7c2e-8a4f-4e7b-9c3d-2f6a1e0b9d47",
"externalId": "cliente-4821",
"status": "approved",
"previousStatus": null,
"decidedBy": "automatic",
"reasons": [],
"occurredAt": "2026-10-27T14:06:12.000Z"
}| Campo | Tipo | Descripción |
|---|---|---|
event | string | kyc.decision, o kyc.test en el envío de prueba |
eventId | string | ID del evento |
verificationId | string | ID de la verificación |
linkId | string | null | El id devuelto por POST /kyc/links |
externalId | string | null | El identificador que enviaste al crear el enlace |
status | string | Decisión: approved, manual_review o rejected. Ver Decisión. |
previousStatus | string | null | La decisión anterior, cuando cambió (por ejemplo, de manual_review a approved) |
decidedBy | string | automatic (reglas de la cuenta) o manual (tu equipo en el dashboard) |
reasons | array | Motivos de la decisión automática: [{ "code", "message" }]. Vacío en decisiones manuales. |
occurredAt | string | Cuándo se tomó la decisión (ISO 8601, UTC) |
Encabezados
| Header | Descripción |
|---|---|
x-cpfhub-event | El mismo valor de event |
x-cpfhub-delivery | ID de la entrega. Úsalo para ignorar repeticiones. |
x-cpfhub-signature | t=<unix>,v1=<hex>: momento del envío en segundos y la firma |
Verificar la firma
v1 es el HMAC-SHA256, en hexadecimal, de "<t>.<cuerpo crudo>" con tu secreto. Calcúlalo sobre el cuerpo exactamente como llegó, antes de cualquier parseo de JSON, compara en tiempo constante y rechaza eventos con t a más de 5 minutos de tu reloj.
import crypto from 'node:crypto'
import express from 'express'
const app = express()
// Cuerpo crudo: la firma se calcula sobre los bytes recibidos
app.post('/webhooks/cpfhub', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('x-cpfhub-signature') ?? ''
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
const t = Number(parts.t)
const expected = crypto
.createHmac('sha256', process.env.CPFHUB_WEBHOOK_SECRET!)
.update(`${parts.t}.${req.body}`)
.digest()
const received = Buffer.from(parts.v1 ?? '', 'hex')
const valid =
received.length === expected.length &&
crypto.timingSafeEqual(received, expected) &&
Math.abs(Date.now() / 1000 - t) < 300
if (!valid) return res.sendStatus(401)
const event = JSON.parse(req.body.toString())
if (event.event === 'kyc.decision') {
// Vincula la decisión con tu registro por el externalId
console.log(event.externalId, event.status)
}
res.sendStatus(200)
})Entrega y reintentos
- Confirmación: responde con cualquier status
2xxen hasta 10 segundos. Procesa el evento después (cola), no antes de responder. - Reintentos: sin
2xx, por timeout o redirección, volvemos a intentar en 1 min, 5 min, 30 min, 2 h y 12 h. Después de eso la entrega queda como fallida, y puedes reenviarla desde el dashboard. - Repeticiones: el mismo evento puede llegar más de una vez. Ignora un
x-cpfhub-deliveryque ya hayas procesado. - Orden: no hay garantía de orden entre eventos. Usa
occurredAty, ante la duda, confirma la decisión actual conGET /kyc/links/{id}. - Destino: solo
httpsy solo direcciones públicas. Las redirecciones no se siguen.
No confíes en la redirección
El regreso de la persona a tu redirectUrl no prueba nada: puede abrir esa URL por su cuenta. Libera el registro solo después de un webhook con firma válida, o de la consulta por la API.
Actualizado el 27 de octubre de 2026