Ir al contenido

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.

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.

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_body
signature = hex(HMAC-SHA256(endpoint_secret, base))
CierreListo-Signature: t=<timestamp>,v1=<signature>
  • timestamp usa segundos Unix.
  • delivery_id es el valor exacto de CierreListo-Delivery.
  • raw_body son los bytes recibidos, interpretados como UTF-8 sin volver a serializar el JSON.
  • v1 identifica 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.

  1. Conserva el cuerpo crudo

    Lee los headers contractuales y los bytes exactos recibidos antes de parsear JSON.

  2. Verifica la firma versionada

    Usa el algoritmo, timestamp y ventana publicados para el ambiente habilitado.

  3. Compara en tiempo constante

    No compares firmas con una igualdad de strings dependiente del tiempo.

  4. Deduplica

    Inserta event_id bajo una restricción única antes de ejecutar efectos.

  5. Confirma recepción

    Responde 2xx después de persistir. No esperes llamadas a otros sistemas.

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.

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.

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.

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.

Preparar certificados y secuencias →