Webhooks
Los webhooks permiten recibir notificaciones HTTP en tiempo real cuando ocurren eventos importantes en tus integraciones de JAAK. En lugar de consultar periódicamente la API, tu servidor recibe automáticamente los datos cuando hay cambios.
Cómo funcionan
1. Configuras una URL de webhook en tu dashboard o al crear recursos
2. Ocurre un evento (ej: sesión completada, documento firmado)
3. JAAK envía un POST a tu URL con los datos del evento
4. Tu servidor procesa el evento y responde con 200 OK
Requisitos de tu endpoint
Tu endpoint debe ser accesible públicamente vía HTTPS y responder en menos de 30 segundos. URLs con HTTP o localhost no son aceptadas en producción.
Configuración
En el Dashboard
- Accede a Configuración → Webhooks en tu dashboard
- Haz clic en Agregar webhook
- Ingresa tu URL de webhook (debe ser HTTPS)
- Selecciona los eventos que deseas recibir
- Guarda la configuración
Vía API
También puedes especificar un webhookUrl al crear recursos individuales:
{
"verificationType": "kyc_full",
"webhookUrl": "https://tu-servidor.com/api/webhooks/jaak",
"redirectUrl": "https://tu-app.com/completado"
}
Estructura del webhook
Cada webhook incluye la siguiente estructura:
{
"id": "evt_abc123xyz",
"event": "session.completed",
"timestamp": "2024-01-15T10:35:00Z",
"apiVersion": "2024-01",
"data": {
// Datos específicos del evento
}
}
| Campo | Descripción | |-------|-------------| | id | Identificador único del evento | | event | Tipo de evento (ver lista abajo) | | timestamp | Fecha y hora del evento en formato ISO 8601 | | apiVersion | Versión de la API utilizada | | data | Objeto con los datos específicos del evento |
Verificación de firma HMAC
Para garantizar que los webhooks provienen de JAAK, cada solicitud incluye una firma en el header X-JAAK-Signature. Debes verificar esta firma antes de procesar el evento.
Header de firma
X-JAAK-Signature: sha256=a1b2c3d4e5f6...
X-JAAK-Timestamp: 1705314900
Proceso de verificación
- Obtén el timestamp del header
X-JAAK-Timestamp - Verifica que el timestamp no tenga más de 5 minutos de antigüedad
- Construye la cadena a firmar:
TIMESTAMP.BODY - Calcula HMAC-SHA256 usando tu webhook secret
- Compara con la firma del header
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, timestamp, secret) {
// Verificar que el timestamp no sea muy antiguo (5 minutos)
const currentTime = Math.floor(Date.now() / 1000);
if (currentTime - parseInt(timestamp) > 300) {
throw new Error('Timestamp demasiado antiguo');
}
// Construir la cadena a firmar
const signedPayload = `${timestamp}.${payload}`;
// Calcular HMAC
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Comparar firmas de forma segura
const receivedSig = signature.replace('sha256=', '');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(receivedSig)
);
}
// Uso en Express
app.post('/webhooks/jaak', (req, res) => {
const signature = req.headers['x-jaak-signature'];
const timestamp = req.headers['x-jaak-timestamp'];
const payload = JSON.stringify(req.body);
if (!verifyWebhookSignature(payload, signature, timestamp, WEBHOOK_SECRET)) {
return res.status(401).send('Firma inválida');
}
// Procesar el evento
const event = req.body;
console.log('Evento recibido:', event.event);
res.status(200).send('OK');
});Seguridad
Nunca omitas la verificación de firma en producción. Sin esta validación, un atacante podría enviar webhooks falsos a tu endpoint.
Política de reintentos
Si tu endpoint no responde con un código 2xx, JAAK reintenta el envío con backoff exponencial:
| Intento | Espera | |---------|--------| | 1 | Inmediato | | 2 | 1 minuto | | 3 | 5 minutos | | 4 | 30 minutos | | 5 | 2 horas | | 6 | 6 horas | | 7 | 12 horas |
Después de 7 intentos fallidos, el webhook se marca como fallido y puedes verlo en el dashboard.
Logs de webhooks
En el dashboard puedes ver el historial de webhooks enviados, incluyendo los que fallaron, con la posibilidad de reenviarlos manualmente.
Eventos de Verificación de Identidad (KYC)
session.started
Se envía cuando el usuario inicia el proceso de verificación.
{
"event": "session.started",
"timestamp": "2024-01-15T10:31:00Z",
"data": {
"sessionId": "ses_abc123xyz789",
"externalId": "user_12345",
"verificationType": "kyc_full",
"status": "processing"
}
}
session.completed
Se envía cuando la verificación se completa exitosamente.
{
"event": "session.completed",
"timestamp": "2024-01-15T10:35:00Z",
"data": {
"sessionId": "ses_abc123xyz789",
"externalId": "user_12345",
"verificationType": "kyc_full",
"status": "completed",
"decision": "approved",
"confidence": 0.97,
"person": {
"fullName": "JUAN PEREZ GARCIA",
"dateOfBirth": "1990-05-15",
"curp": "PEGJ900515HDFRRS09"
}
}
}
session.failed
Se envía cuando la verificación falla por error técnico o intento de fraude.
{
"event": "session.failed",
"timestamp": "2024-01-15T10:33:00Z",
"data": {
"sessionId": "ses_abc123xyz789",
"externalId": "user_12345",
"status": "failed",
"failureReasons": [
{
"code": "LIVENESS_FAILED",
"message": "No se pudo verificar que hay una persona real"
}
]
}
}
session.expired
Se envía cuando la sesión expira sin completarse.
{
"event": "session.expired",
"timestamp": "2024-01-15T11:30:00Z",
"data": {
"sessionId": "ses_abc123xyz789",
"externalId": "user_12345",
"status": "expired"
}
}
Eventos de Firma Digital (Signa)
signature.requested
Se envía cuando se solicita una firma a un firmante.
{
"event": "signature.requested",
"timestamp": "2024-01-15T14:00:00Z",
"data": {
"submissionId": "sub_xyz789abc",
"signerId": "sig_abc123",
"signerEmail": "firmante@empresa.com",
"documentName": "Contrato de servicios"
}
}
signature.completed
Se envía cuando un firmante completa su firma.
{
"event": "signature.completed",
"timestamp": "2024-01-15T14:15:00Z",
"data": {
"submissionId": "sub_xyz789abc",
"signerId": "sig_abc123",
"signerEmail": "firmante@empresa.com",
"signatureType": "electronic_advanced",
"signedAt": "2024-01-15T14:15:00Z"
}
}
signature.rejected
Se envía cuando un firmante rechaza firmar.
{
"event": "signature.rejected",
"timestamp": "2024-01-15T14:10:00Z",
"data": {
"submissionId": "sub_xyz789abc",
"signerId": "sig_abc123",
"signerEmail": "firmante@empresa.com",
"rejectionReason": "No estoy de acuerdo con los términos"
}
}
submission.completed
Se envía cuando todos los firmantes han completado sus firmas.
{
"event": "submission.completed",
"timestamp": "2024-01-15T14:30:00Z",
"data": {
"submissionId": "sub_xyz789abc",
"templateId": "tpl_def456",
"documentName": "Contrato de servicios",
"status": "completed",
"completedAt": "2024-01-15T14:30:00Z",
"signers": [
{
"email": "firmante1@empresa.com",
"signedAt": "2024-01-15T14:15:00Z"
},
{
"email": "firmante2@empresa.com",
"signedAt": "2024-01-15T14:30:00Z"
}
],
"downloadUrl": "https://signa.jaak.ai/api/v1/submissions/sub_xyz789abc/download"
}
}
submission.cancelled
Se envía cuando se cancela un envío.
{
"event": "submission.cancelled",
"timestamp": "2024-01-15T15:00:00Z",
"data": {
"submissionId": "sub_xyz789abc",
"cancelledBy": "usr_admin123",
"reason": "Documento incorrecto"
}
}
Mejores prácticas
1. Responde rápidamente
Tu endpoint debe responder con 200 OK en menos de 30 segundos. Si necesitas hacer procesamiento pesado, hazlo de forma asíncrona:
app.post('/webhooks/jaak', async (req, res) => {
// Responder inmediatamente
res.status(200).send('OK');
// Procesar en segundo plano
processEventAsync(req.body);
});
2. Implementa idempotencia
Los webhooks pueden enviarse más de una vez. Usa el id del evento para evitar procesar duplicados:
async function processEvent(event) {
// Verificar si ya procesamos este evento
const exists = await db.events.findOne({ eventId: event.id });
if (exists) {
console.log('Evento ya procesado:', event.id);
return;
}
// Procesar el evento
await handleEvent(event);
// Marcar como procesado
await db.events.insertOne({ eventId: event.id, processedAt: new Date() });
}
3. Maneja todos los tipos de evento
Aunque solo necesites ciertos eventos, tu endpoint debe manejar cualquier tipo sin fallar:
function handleEvent(event) {
switch (event.event) {
case 'session.completed':
return handleSessionCompleted(event.data);
case 'signature.completed':
return handleSignatureCompleted(event.data);
default:
console.log('Evento no manejado:', event.event);
}
}
4. Registra todos los webhooks
Guarda un log de todos los webhooks recibidos para debugging:
app.post('/webhooks/jaak', async (req, res) => {
// Log del webhook
await db.webhookLogs.insertOne({
receivedAt: new Date(),
headers: req.headers,
body: req.body
});
// Procesar...
res.status(200).send('OK');
});
Solución de problemas
Mi endpoint no recibe webhooks
- Verifica que la URL sea accesible públicamente
- Confirma que uses HTTPS (HTTP no es aceptado)
- Revisa que tu firewall permita conexiones desde JAAK
- Comprueba el historial de webhooks en el dashboard
Los webhooks llegan tarde
Los webhooks se envían en tiempo real, pero pueden retrasarse si:
- Tu endpoint está lento o no responde
- Hay reintentos por errores previos
- Hay alto volumen de eventos
Errores de verificación de firma
- Asegúrate de usar el webhook secret correcto
- Verifica que no estés modificando el body antes de verificar
- Comprueba que el timestamp no tenga más de 5 minutos
Siguientes pasos
- Errores - Códigos de error y cómo resolverlos
- Sandbox - Prueba webhooks en ambiente de desarrollo
- API de Sesiones - Crea sesiones de verificación