# Entrega, reintentos y replay (/docs/webhooks/entrega)

## El contrato de entrega [#el-contrato-de-entrega]

* Responde `2xx` en **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 cuyo `data.object` sea más
  viejo que el que ya tienes. Que `document.accepted` llegue antes que `document.sent` es normal y no
  puede romperte.

## El calendario de reintentos [#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 [#inspeccionar-y-reintentar-a-mano]

```bash
# 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 [#el-libro-de-eventos]

```bash
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"
```

### Polling en vez de webhooks [#polling-en-vez-de-webhooks]

Si no puedes exponer un endpoint HTTPS —un agente personal, un laptop, cualquier cosa detrás de
NAT— `GET /v1/events?after=` es el sustituto soportado, no un rodeo:

```bash
curl -G https://api.facturia.cl/v1/events \
  -H "Authorization: Bearer $FACTURIA_KEY" \
  -d "after=evt_01K2RG1P4M7X" -d "limit=100"
```

`after` pagina **hacia adelante**: más viejo primero, estrictamente más nuevo que el id que nombras,
y `next_cursor` avanza alejándose de él. El loop es `after = next_cursor ?? after`, procesando `data`
en orden; nunca vuelve a leer una fila y no necesita un set de ids vistos. `after` acepta tanto un id
`evt_…` (el último evento que procesaste) como un valor `cur_…` de `next_cursor`, así que tu
checkpoint guardado y el token que te devuelven son intercambiables. Cuando `has_more` es `false`,
`next_cursor` es `null` — guarda el `id` del último evento procesado y mándalo la próxima vez.

`cursor` es la dirección contraria y la que se usa por defecto: más nuevo primero, «más viejo que
esto», que es lo que quiere un panel leyendo el libro hacia atrás. Mandar los dos a la vez es `400`
en vez de una precedencia silenciosa — nombran posiciones en direcciones opuestas, y honrar
cualquiera de los dos saltaría filas para la mitad de quienes llaman sin decírselo.

> **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 [#un-consumidor-que-se-porta-bien]

```js
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.
