WebhookDelivery
Un WebhookDelivery es un intento de hacer POST de un payload de webhook a su URL de destino. El dispatcher crea una fila por intento; los reintentos producen filas adicionales vinculadas al mismo system_event_id.
Propósito
- Proveer un historial auditable de cada intento de webhook (status, payload, response, duración).
- Permitir la repetición (replay) de entregas fallidas sin volver a emitir el evento subyacente.
- Alimentar la UI de webhook deliveries en
/manage.
Schema
| Campo | Tipo | Descripción |
|---|---|---|
id | UUID | ID interno. |
webhook_id | UUID | Webhook propietario. |
system_event_id | UUID | El evento que se está entregando. |
attempt | int | Contador de reintentos, indexado desde 1. |
status | enum | pending, success, failed. |
request_headers | object | Headers salientes (valores de auth redactados). |
request_body | object | El payload JSON enviado. |
response_status | int | null | Status HTTP recibido. |
response_headers | object | null | Headers del destino (limitado a 4 KiB). |
response_body | string | null | Body del destino (limitado a 16 KiB). |
error | string | null | Error de transporte (timeout, fallo de DNS, TLS). |
duration_ms | int | null | Tiempo real de la llamada HTTP. |
delivered_at | datetime | null | Cuándo terminó la llamada. |
next_retry_at | datetime | null | Cuándo está programado el próximo reintento (si lo hay). |
created_at / updated_at | datetime |
Estado
| Estado | Significado |
|---|---|
pending | En cola o en curso; delivered_at no está establecido. |
success | Se recibió 2xx dentro del timeout. |
failed | No-2xx, timeout o error de transporte. Puede reintentar — revisa next_retry_at. |
Ejemplo
{
"id": "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d",
"webhook_id": "f6a7b8c9-0d1e-4f2a-3b4c-5d6e7f8a9b0c",
"system_event_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"attempt": 2,
"status": "success",
"request_headers": {
"X-Kobana-Event": "email.db.created",
"X-Kobana-Timestamp": "1747490400",
"X-Kobana-Signature": "sha256=…",
"Authorization": "Bearer [REDACTED]"
},
"request_body": { "event": "email.db.created", "data": {} },
"response_status": 200,
"response_headers": { "content-type": "application/json" },
"response_body": "{\"ok\":true}",
"error": null,
"duration_ms": 312,
"delivered_at": "2026-05-17T12:05:14.218Z",
"next_retry_at": null,
"created_at": "2026-05-17T12:05:13.906Z",
"updated_at": "2026-05-17T12:05:14.218Z"
}
Lectura
| Método | Path |
|---|---|
GET | /webhooks/{id}/deliveries?status=failed&attempt[gte]=2 |
GET | /webhooks/{id}/deliveries/{deliveryId} |
Filtros: status, attempt[gte|lt], created_at[gte|lt].
Replay
POST /webhooks/{id}/deliveries/{deliveryId}/replay encola una redelivery inmediata — ignora cualquier backoff pendiente, pero se registra como una nueva fila (attempt no se incrementa; se agrega un nuevo WebhookDelivery con attempt=1 y una referencia metadata.replay_of).
Cuando cualquier entrega de un evento a un webhook tiene éxito — replay incluido — los reintentos aún programados para ese evento no se envían, y el next_retry_at de sus filas failed anteriores se limpia. Un replay exitoso, por lo tanto, termina la cadena de backoff de la entrega original.
La solicitud del replay también lleva su propio X-Idempotency-Key, un valor nuevo y aleatorio en lugar del id del evento que usa cada intento automático (ver Headers). Los reintentos automáticos se siguen deduplicando contra el intento original a propósito; un replay es una solicitud deliberada de actuar sobre el evento de nuevo, así que no debe ser absorbido por la misma guarda.