> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bbrands.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Reintentos y fallos

> Cómo se clasifica cada respuesta, el calendario de backoff, el estado exhausted, la desactivación automática tras fallos consecutivos y cómo reenvían los operadores.

## Estados de una entrega

Cada entrega es una fila en `webhook_dispatch`. Su `status` evoluciona así:

```mermaid theme={null}
stateDiagram-v2
    [*] --> pending: enrutada
    pending --> delivering: el worker la toma
    delivering --> success: 2xx
    delivering --> error: fallo reintentable (next_retry_at fijado)
    delivering --> error: fallo terminal (sin reintento)
    error --> delivering: reintento vencido
    delivering --> exhausted: fallo reintentable en el intento 5
    success --> pending: replay (confirm_duplicate)
    error --> pending: replay
    exhausted --> pending: replay
```

| `status`     | Significado                                                                                                        |
| ------------ | ------------------------------------------------------------------------------------------------------------------ |
| `pending`    | Enrutada, a la espera del worker de entrega.                                                                       |
| `delivering` | Hay un `POST` en curso.                                                                                            |
| `success`    | El suscriptor respondió `2xx`. `delivered_at` queda fijado.                                                        |
| `error`      | El último intento falló. Si `next_retry_at` tiene valor hay un reintento programado; si no, el fallo fue terminal. |
| `exhausted`  | El quinto intento falló con un error reintentable. No hay más intentos automáticos.                                |

`attempts`, `last_http_status` y `last_error` se actualizan en cada intento,
de modo que el libro mayor se explica solo. `attempts` es el recuento de
POST de toda la vida de esa fila: un replay del operador no lo pone a cero.

## Matriz de resultados

| Respuesta del suscriptor                                           | Clasificación | Efecto                                                                                                                              |
| ------------------------------------------------------------------ | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `2xx`                                                              | Éxito         | `status = success`; `consecutive_failures` del webhook vuelve a `0`.                                                                |
| `408`, `429`, cualquier `5xx`                                      | Reintentable  | `status = error`, `next_retry_at` programado; `exhausted` en el último intento.                                                     |
| Timeout (10 s), fallo DNS/TCP/TLS                                  | Reintentable  | Igual que arriba.                                                                                                                   |
| Cualquier otro `4xx` (`400`, `401`, `403`, `404`, `410`, `422`, …) | Terminal      | `status = error` sin reintento. Cuenta para la desactivación automática.                                                            |
| Cualquier `3xx`                                                    | Terminal      | Las redirecciones nunca se siguen (un cuerpo firmado no debe reenviarse a un tercer host). Cuenta para la desactivación automática. |

<Note>
  Solo los resultados **terminales** y **exhausted** incrementan
  `consecutive_failures`. Un `503` que se recupera en el siguiente reintento
  no es una entrega perdida y no mueve el cortacircuitos.
</Note>

## Calendario de backoff

Los fallos reintentables se reenvían con un calendario fijo, visible para el
suscriptor (`WEBHOOK_OUT_MAX_ATTEMPTS = 5`):

| Intento   | Espera previa                                                                      | Acumulado |
| --------- | ---------------------------------------------------------------------------------- | --------- |
| 1         | —                                                                                  | 0         |
| 2         | 1 minuto                                                                           | 1 m       |
| 3         | 5 minutos                                                                          | 6 m       |
| 4         | 30 minutos                                                                         | 36 m      |
| 5         | 2 horas                                                                            | 2 h 36 m  |
| tras el 5 | `exhausted` (hay una ranura de 6 horas reservada que nunca se usa automáticamente) | —         |

`next_retry_at` en la fila del libro mayor muestra la hora exacta del
próximo intento. El campo `attempt` del cuerpo se incrementa mientras `id`
permanece igual.

## Desactivación automática

Un endpoint muerto no debe consumir capacidad de cola indefinidamente.
Cuando un webhook acumula **20 entregas consecutivas** terminales o
exhausted:

* `is_actived` pasa a `false`,
* se estampa `disabled_at` y `disabled_reason = "consecutive-failures"`,
* la etapa route deja de escribir filas de dispatch para él,
* el componente de salud `webhook-out` y el resumen del dashboard lo cuentan
  en `disabled_auto`.

Las entregas ya en vuelo terminan su intento actual; no se programa nada
nuevo.

### Reactivar

1. Arregla el endpoint (o su URL mediante `PATCH`).
2. Envía una entrega de prueba: `POST /api/v3/webhook/webhook/{id}/test`.
3. Activa: `PATCH /api/v3/webhook/webhook/{id}/active` con
   `{ "is_actived": true }`. Esto limpia `consecutive_failures`,
   `disabled_at` y `disabled_reason`.
4. Reenvía lo que se perdió (abajo).

Existe otro `disabled_reason`: `legacy-secret-rotation-required`. Los
webhooks anteriores a este release fueron desactivados por la migración
`0237` porque sus secretos son previos al contrato de firma; rota el secreto
y después activa.

## Replay

`POST /api/v3/webhook/dispatch/{id}/replay` reencola una fila del libro mayor
(`status = pending`, último error y `next_retry_at` limpiados) **sin
reiniciar `attempts`**. El siguiente POST incrementa el mismo contador, así
que la fila conserva el historial completo de entregas. El suscriptor recibe
el mismo `id`, `event` y `data`; `attempt` es el siguiente número secuencial.

* Reenviar una fila `error` o `exhausted` no necesita cuerpo.
* Reenviar una fila `success` devuelve `409 Conflict` salvo que el cuerpo
  incluya `{ "confirm_duplicate": true }`, porque el suscriptor verá el
  evento dos veces. Los endpoints que deduplican por `id` lo gestionan de
  forma transparente.

Los reintentos automáticos siguen deteniéndose a los cinco POST
(`WEBHOOK_OUT_MAX_ATTEMPTS`). Un replay del operador sobre una fila
`exhausted` hace un POST adicional; si ese POST es reintentable, la fila
vuelve a `exhausted` (sin nueva programación automática).

Horizon Enterprise expone la misma acción por fila en la pantalla de detalle
del webhook, con filtros por estado, evento y fecha.

<Warning>
  El replay no resucita el recurso origen. Si la fila fue purgada desde
  entonces, `data.document_id` sigue apuntando al id antiguo y tu lectura por
  la API devolverá `404` — trátalo como "ya no existe".
</Warning>
