Webhooks, firma y reintentos
Los webhooks notifican transiciones sin obligarte a consultar continuamente. La entrega es al menos una vez: el mismo evento puede llegar varias veces y el orden global no está garantizado.
Endpoint receptor
Sección titulada «Endpoint receptor»Tu endpoint debe:
- usar HTTPS público;
- leer el cuerpo crudo antes de parsear JSON;
- verificar firma y antigüedad;
- deduplicar por
event.id; - responder rápido después de persistir;
- procesar el efecto de negocio fuera del request;
- rechazar tamaños o formatos inesperados.
Verificación de firma
Sección titulada «Verificación de firma»El secreto pertenece al endpoint, no a la API key. El contrato actual declara los headers
CierreListo-Signature, CierreListo-Delivery y CierreListo-Event.
La firma vigente usa HMAC-SHA256:
base = timestamp + "." + delivery_id + "." + raw_bodysignature = hex(HMAC-SHA256(endpoint_secret, base))CierreListo-Signature: t=<timestamp>,v1=<signature>timestampusa segundos Unix.delivery_ides el valor exacto deCierreListo-Delivery.raw_bodyson los bytes recibidos, interpretados como UTF-8 sin volver a serializar el JSON.v1identifica la versión del esquema de firma, no la versión del evento.
Recomendamos una tolerancia de cinco minutos, sincronización NTP y comparación en tiempo constante.
Una entrega reintentada conserva su delivery_id, pero recibe un timestamp y una firma nuevos.
-
Conserva el cuerpo crudo
Lee los headers contractuales y los bytes exactos recibidos antes de parsear JSON.
-
Verifica la firma versionada
Usa el algoritmo, timestamp y ventana publicados para el ambiente habilitado.
-
Compara en tiempo constante
No compares firmas con una igualdad de strings dependiente del tiempo.
-
Deduplica
Inserta
event_idbajo una restricción única antes de ejecutar efectos. -
Confirma recepción
Responde
2xxdespués de persistir. No esperes llamadas a otros sistemas.
Verificador TypeScript
Sección titulada «Verificador TypeScript»El SDK oficial aplica el mismo contrato que genera OpenAPI: verifica HMAC-SHA256, antigüedad, headers, tipo del evento y schema completo.
import { verifyCierreListoWebhook } from "@cierrelisto/sdk";
const verification = await verifyCierreListoWebhook({ rawBody, headers: request.headers, endpointSecret: process.env.CIERRELISTO_WEBHOOK_SECRET ?? "",});
if (!verification.ok) { console.error(verification.reason); return new Response("Webhook inválido", { status: 400 });}
const event = verification.event;if (event.type === "ecf.accepted") { console.log(event.data.encf);}Durante la ventana de rotación, endpointSecret también acepta
[secretoActual, secretoAnterior]. El helper usa Web Crypto y funciona en Node.js 22 o superior.
Eventos y estado
Sección titulada «Eventos y estado»El catálogo exacto se genera desde OpenAPI. En v1 se publican:
ecf.submitted;ecf.accepted;ecf.accepted_conditional;ecf.rejected;ecf.outcome_unknown.
Un webhook contiene referencias estables y estado; los artefactos pesados se consultan por API.
Valida también CierreListo-Event contra el campo type del cuerpo.
Reintentos y reenvío controlado
Sección titulada «Reintentos y reenvío controlado»CierreListo reintenta fallos transitorios con backoff y conserva historial de entregas. Un 4xx
persistente puede deshabilitar gradualmente el endpoint para proteger ambas partes.
La consola autenticada permite:
- rotar el secreto con solapamiento;
- deshabilitar el endpoint sin borrar su historial.
Procesamiento idempotente
Sección titulada «Procesamiento idempotente»La clave primaria de tu inbox debe ser el campo id del evento, no el número de intento. Conserva
también CierreListo-Delivery para diagnosticar reintentos de transporte. Un reenvío operativo
puede usar otro delivery_id para el mismo evento. Si event.id ya existe, responde exitosamente
sin repetir el efecto.