Entrega, reintentos y replay
El calendario de reintentos con jitter, el auto-apagado a los 5 días, y por qué el libro de eventos vive 12 meses.
El contrato de entrega
- Responde
2xxen 10 segundos. Cualquier otra cosa — código distinto de 2xx, timeout, fallo de TLS — cuenta como falla. Haz tu trabajo de forma asíncrona: acusa primero. - Entrega al menos una vez y sin orden garantizado. Los eventos pueden llegar dos veces y
desordenados. Deduplica por
id(evt_…) e ignora cualquier evento cuyodata.objectsea más viejo que el que ya tienes. Quedocument.acceptedllegue antes quedocument.sentes normal y no puede romperte.
El calendario de reintentos
Inmediatamente, y después 30 s, 2 min, 10 min, 1 h, 3 h, 6 h, 12 h, 24 h — 9 intentos en unas 48 horas, con jitter.
El jitter existe para que una caída de tu lado no produzca una estampida sincronizada cuando vuelvas.
Trampa
A los 5 días de falla total, el endpoint se apaga solo
Después de 5 días corridos de falla total, el endpoint pasa a status: disabled, mandamos un
correo a la cuenta y emitimos webhook_endpoint.disabled.
Un endpoint apagado no acumula reintentos: los eventos siguen en el libro, pero nadie te los está empujando. Reactivarlo no reenvía lo perdido automáticamente — para eso está el replay.
Inspeccionar y reintentar a mano
# Qué intentamos entregar, y cómo fue
curl -G https://api.facturia.cl/v1/webhook_endpoints/whe_01K2RF5N8Q2W/deliveries \
-H "Authorization: Bearer $FACTURIA_KEY" -d status=failed
# Reintentar una entrega puntual
curl -X POST https://api.facturia.cl/v1/webhook_endpoints/whe_01K2RF5N8Q2W/deliveries/whd_01K2RG1P4M7X01/retry \
-H "Authorization: Bearer $FACTURIA_KEY"El libro de eventos
curl -G https://api.facturia.cl/v1/events \
-H "Authorization: Bearer $FACTURIA_KEY" \
-d "type=document.accepted" -d "created_at[gte]=2026-08-14T00:00:00Z"
curl https://api.facturia.cl/v1/events/evt_01K2RG1P4M7X \
-H "Authorization: Bearer $FACTURIA_KEY"12 meses de retención, a propósito
Los eventos se retienen 12 meses — deliberadamente más que el libro de webhooks habitual de 30 a 90 días, porque en este dominio la unidad de trabajo es un año tributario.
Una diferencia que aparece cerrando un período en marzo necesita que los eventos del julio anterior sigan estando, y reconstruir un ciclo completo de F29 desde el libro es un flujo soportado, no una emergencia.
Si tu consumidor estuvo caído una semana, haz replay en vez de reconciliar a mano.
Un consumidor que se porta bien
const procesados = new Set(); // en producción, una tabla con índice único
export async function manejar(evento) {
if (procesados.has(evento.id)) return; // al menos una vez
switch (evento.type) {
case 'document.accepted':
await marcarAceptado(evento.data.object);
break;
case 'document.rejected':
case 'document.reparo':
await avisarAOperaciones(evento.data.object);
break;
case 'received_document.deadline_approaching':
await avisarPlazo(evento.data.object);
break;
default:
// Tipos de evento nuevos aparecen sin previo aviso: ignóralos, no lances.
break;
}
procesados.add(evento.id);
}Esa rama default es la única regla de compatibilidad que te pedimos. Dentro de /v1 evolucionamos
de forma aditiva, y un cliente que se cae con un tipo de evento desconocido se va a caer.