Skip to content
ES

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.

MétodoRutaAuthDescripción
POST/v1/webhooksAPI keyRegistrar un endpoint
GET/v1/webhooksAPI keyListar endpoints (secret enmascarado)
DELETE/v1/webhooks/:idAPI keyEliminar un endpoint
POST/v1/webhooks/:id/testAPI keyEnviar un evento ping de prueba
GET/v1/webhooks/:id/deliveriesAPI keyÚltimas 50 entregas
EventoCuándo se emite
session.completedSesión aprobada (verificación completa)
session.declinedSesión rechazada
session.reviewSesión requiere revisión manual
pingEvento de prueba (POST /:id/test)
POST /v1/webhooks
X-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"
}
CampoTipoRequeridoDescripción
urlstring (URL) ≤2000Destino de las entregas
eventsstring[] ≤50NoEventos a los que suscribirse (default [])
secretstring 16–80NoSecreto 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.
GET /v1/webhooks
X-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).

DELETE /v1/webhooks/3
POST /v1/webhooks/3/test
GET /v1/webhooks/3/deliveries

POST /: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");

Cada entrega saliente incluye estos headers:

HeaderContenido
X-Zentto-SignatureHMAC-SHA256 del canonical string, en hex
X-Zentto-Created-AtEpoch en segundos usado en el canonical
X-Zentto-EventTipo 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) // hex

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.

Vista no técnica: cómo registrar tu destino y qué ocurre cuando el sistema te avisa automáticamente.

Flujo del usuario — KYC · Webhooks

Editable en draw.io: descarga el SVG → en draw.io: File → Import from → Device → selecciona el SVG. Cada nodo queda editable.

Vista técnica: evento → firma HMAC-SHA256 → POST al endpoint cliente → verificación + reintentos.

Flujo técnico — KYC · Webhooks

ComponenteTipoUbicación
POST /v1/webhooksRoute Expresssrc/webhooks/routes.ts
GET /v1/webhooksRoute Expresssrc/webhooks/routes.ts
POST /v1/webhooks/:id/testRoute Expresssrc/webhooks/routes.ts
GET /v1/webhooks/:id/deliveriesRoute Expresssrc/webhooks/routes.ts
DELETE /v1/webhooks/:idRoute Expresssrc/webhooks/routes.ts
src/webhooks/dispatcher.tsCola de entregas + reintentos exponencialessrc/webhooks/dispatcher.ts
webhook_endpointsTabla endpoints (url, events, secret hash)src/db/migrations/
webhook_deliveriesTabla historial entregas (status_code, attempts)src/db/migrations/
verifyKycWebhook()Verificación HMAC en receptor NodeSnippet de integración

Editable en draw.io: descarga el SVG → File → Import from → Device.