Saltar al contenido
Klevy.

Webhooks

Un run contra un portal del Estado puede tardar minutos. Sostener una petición HTTP ese tiempo es frágil, así que POST /v1/runs responde 202 y el resultado llega acá.

Registrar un endpoint

Con POST /v1/webhook_endpoints, o pasando webhook_url al crear una sesión. La respuesta trae el webhook_secret una sola vez: si no lo guardas en ese momento, hay que rotarlo.

Eventos

EventoCuándo
run.completedEl run terminó bien. Trae los datos normalizados y los enlaces a sus documentos.
run.failedEl run falló. Trae el code del catálogo de errores.
session.completedTodos los runs de una sesión del widget terminaron.
session.failedLa sesión terminó mal — clave rechazada, expirada o sin runs que ejecutar.

Verificar la firma

Cada entrega va firmada con HMAC-SHA256 sobre {timestamp}.{cuerpo}, usando tu webhook_secret. Verifica antes de confiar en el cuerpo y antes de parsearlo: si no, tu sistema procesa lo que cualquiera le mande.

import { createHmac, timingSafeEqual } from 'node:crypto';

export function firmaValida(cuerpoCrudo, cabecera, secreto) {
  // klevy-signature: t=1754700000,v1=9f86d081...
  const partes = Object.fromEntries(
    cabecera.split(',').map((p) => p.split('=')),
  );
  if (!partes.t || !partes.v1) return false;

  // Una firma vieja es una firma robada: se rechaza pasados 5 minutos.
  const edad = Math.abs(Date.now() / 1000 - Number(partes.t));
  if (edad > 300) return false;

  const esperada = createHmac('sha256', secreto)
    .update(`${partes.t}.${cuerpoCrudo}`)
    .digest('hex');

  // Comparación en tiempo constante: un `===` filtra el secreto byte a byte.
  const a = Buffer.from(esperada, 'hex');
  const b = Buffer.from(partes.v1, 'hex');
  return a.length === b.length && timingSafeEqual(a, b);
}
Firma sobre el cuerpo crudo, no sobre el JSON reserializado. Volver a serializar reordena claves y cambia espacios, y entonces ninguna firma legítima calza.

Reintentos

Se considera entregado cualquier 2xx. Cualquier otra cosa —o un timeout— se reintenta con retroceso exponencial. Responde rápido y procesa después: si tu endpoint tarda, la entrega se cuenta como fallida aunque la hayas recibido.

Una entrega puede repetirse. Trata el id del evento como clave de idempotencia y descarta el que ya procesaste.

Ejemplo de entrega

POST /webhooks/klevy
klevy-signature: t=1754700000,v1=9f86d081884c7d65...
content-type: application/json

{
  "id": "evt_7hq2n4kx0m1pz8v",
  "type": "run.completed",
  "created_at": "2026-08-09T12:34:07.000Z",
  "livemode": true,
  "data": {
    "run_id": "run_3kf82ncq7p1xw0a",
    "flow": "sii.rcv_resumen",
    "status": "completed",
    "session_id": "ses_ra1h4fzpatgae33c"
  }
}