API — Webhooks
This content is not available in your language yet.
Los webhooks notifican a tu backend cuando una sesión cambia de estado, sin polling. Cada entrega va firmada con HMAC-SHA256 para que puedas verificar su autenticidad. Requieren API key (X-API-Key: zkyc_...).
Prefijo: /v1/webhooks.
Endpoints
Sección titulada «Endpoints»| Método | Ruta | Auth | Descripción |
|---|---|---|---|
| POST | /v1/webhooks | API key | Registrar un endpoint |
| GET | /v1/webhooks | API key | Listar endpoints (secret enmascarado) |
| DELETE | /v1/webhooks/:id | API key | Eliminar un endpoint |
| POST | /v1/webhooks/:id/test | API key | Enviar un evento ping de prueba |
| GET | /v1/webhooks/:id/deliveries | API key | Últimas 50 entregas |
Eventos
Sección titulada «Eventos»| Evento | Cuándo se emite |
|---|---|
session.completed | Sesión aprobada (verificación completa) |
session.declined | Sesión rechazada |
session.review | Sesión requiere revisión manual |
ping | Evento de prueba (POST /:id/test) |
Registrar un endpoint
Sección titulada «Registrar un endpoint»POST /v1/webhooksX-API-Key: zkyc_...Content-Type: application/json
{ "url": "https://miapp.com/kyc/webhook", "events": ["session.completed", "session.declined", "session.review"], "secret": "mi-secreto-de-al-menos-16-chars"}| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
url | string (URL) ≤2000 | Sí | Destino de las entregas |
events | string[] ≤50 | No | Eventos a los que suscribirse (default []) |
secret | string 16–80 | No | Secreto HMAC. Si se omite, se genera uno |
Respuesta 201 Created — el secret se devuelve una sola vez:
{ "ok": true, "endpoint": { "id": 3, "url": "https://miapp.com/kyc/webhook", "events": ["session.completed", "session.declined", "session.review"], "is_active": true, "created_at": "2026-06-21T10:00:00.000Z" }, "secret": "mi-secreto-de-al-menos-16-chars", "secretGenerated": false}const res = await kyc.webhooks.create({ url: "https://miapp.com/kyc/webhook", events: ["session.completed", "session.declined", "session.review"],});// Guarda res.secret: no se vuelve a mostrar.Listar endpoints
Sección titulada «Listar endpoints»GET /v1/webhooksX-API-Key: zkyc_...{ "ok": true, "endpoints": [ { "id": 3, "url": "https://miapp.com/kyc/webhook", "events": ["session.completed"], "isActive": true, "secretPrefix": "mi-sec…", "createdAt": "2026-06-21T10:00:00.000Z" } ]}En el listado el secret aparece enmascarado (secretPrefix).
Eliminar / probar / entregas
Sección titulada «Eliminar / probar / entregas»DELETE /v1/webhooks/3POST /v1/webhooks/3/testGET /v1/webhooks/3/deliveriesPOST /:id/test encola un evento ping dirigido a ese endpoint y responde 202 Accepted:
{ "ok": true, "message": "Evento de prueba encolado", "deliveryId": "d1e2f3a4-..." }GET /:id/deliveries devuelve las últimas 50 entregas:
{ "ok": true, "deliveries": [ { "id": "d1e2f3a4-...", "event_type": "session.completed", "status_code": 200, "attempts": 1, "next_retry_at": null, "delivered": true, "created_at": "2026-06-21T10:05:00.000Z" } ]}await kyc.webhooks.list();await kyc.webhooks.test("3");await kyc.webhooks.deliveries("3");await kyc.webhooks.delete("3");Esquema de firma
Sección titulada «Esquema de firma»Cada entrega saliente incluye estos headers:
| Header | Contenido |
|---|---|
X-Zentto-Signature | HMAC-SHA256 del canonical string, en hex |
X-Zentto-Created-At | Epoch en segundos usado en el canonical |
X-Zentto-Event | Tipo de evento (session.completed, etc.) |
El canonical string que se firma es:
${createdAt}.${JSON.stringify(payload)}donde createdAt es el valor del header X-Zentto-Created-At y payload es el cuerpo del webhook. La firma se calcula con tu secret:
X-Zentto-Signature = HMAC-SHA256(`${createdAt}.${rawBody}`, secret) // hexVerificar una entrega (Node)
Sección titulada «Verificar una entrega (Node)»El consumidor recomputa el HMAC sobre el raw body recibido (no re-serialices el JSON parseado), compara de forma timing-safe y rechaza si el timestamp está fuera de la ventana ±300s (anti-replay).
import crypto from "node:crypto";
/** * Verifica un webhook entrante de Zentto KYC. * @param rawBody cuerpo crudo de la request (string), sin parsear. * @param headers headers de la request. * @param secret secret del endpoint registrado. */function verifyKycWebhook( rawBody: string, headers: Record<string, string | undefined>, secret: string,): boolean { const signature = headers["x-zentto-signature"]; const createdAt = Number(headers["x-zentto-created-at"]); if (!signature || !Number.isFinite(createdAt)) return false;
// Ventana anti-replay de ±300s. const now = Math.floor(Date.now() / 1000); if (Math.abs(now - createdAt) > 300) return false;
const canonical = `${createdAt}.${rawBody}`; const expected = crypto.createHmac("sha256", secret).update(canonical).digest("hex");
const a = Buffer.from(expected, "hex"); const b = Buffer.from(signature, "hex"); if (a.length !== b.length || a.length === 0) return false; return crypto.timingSafeEqual(a, b);}Ejemplo en Express (asegúrate de capturar el raw body antes del parser JSON):
import express from "express";
const app = express();
app.post( "/kyc/webhook", express.raw({ type: "application/json" }), (req, res) => { const rawBody = req.body.toString("utf8"); const ok = verifyKycWebhook(rawBody, req.headers as any, process.env.KYC_WEBHOOK_SECRET!); if (!ok) return res.status(401).end();
const event = req.headers["x-zentto-event"]; const payload = JSON.parse(rawBody); // Procesa el evento (session.completed, session.declined, session.review, ping) res.status(200).end(); },);Rechaza cualquier entrega cuya firma no coincida o cuyo createdAt esté fuera de la ventana de 300 segundos.
Flujo del usuario
Sección titulada «Flujo del usuario»Vista no técnica: cómo registrar tu destino y qué ocurre cuando el sistema te avisa automáticamente.
Editable en draw.io: descarga el SVG → en draw.io: File → Import from → Device → selecciona el SVG. Cada nodo queda editable.
Flujo técnico
Sección titulada «Flujo técnico»Vista técnica: evento → firma HMAC-SHA256 → POST al endpoint cliente → verificación + reintentos.
| Componente | Tipo | Ubicación |
|---|---|---|
POST /v1/webhooks | Route Express | src/webhooks/routes.ts |
GET /v1/webhooks | Route Express | src/webhooks/routes.ts |
POST /v1/webhooks/:id/test | Route Express | src/webhooks/routes.ts |
GET /v1/webhooks/:id/deliveries | Route Express | src/webhooks/routes.ts |
DELETE /v1/webhooks/:id | Route Express | src/webhooks/routes.ts |
src/webhooks/dispatcher.ts | Cola de entregas + reintentos exponenciales | src/webhooks/dispatcher.ts |
webhook_endpoints | Tabla endpoints (url, events, secret hash) | src/db/migrations/ |
webhook_deliveries | Tabla historial entregas (status_code, attempts) | src/db/migrations/ |
verifyKycWebhook() | Verificación HMAC en receptor Node | Snippet de integración |
Editable en draw.io: descarga el SVG → File → Import from → Device.