WebhookDelivery
A WebhookDelivery is one attempt to POST a webhook payload to its target URL. The dispatcher creates one row per attempt; retries produce additional rows linked to the same system_event_id.
Purpose
- Provide an auditable history of every webhook attempt (status, payload, response, duration).
- Allow replay of failed deliveries without re-emitting the underlying event.
- Power the webhook deliveries UI in
/manage.
Schema
| Field | Type | Description |
|---|---|---|
id | UUID | Internal id. |
webhook_id | UUID | Owning webhook. |
system_event_id | UUID | The event being delivered. |
attempt | int | 1-indexed retry counter. |
status | enum | pending, success, failed. |
request_headers | object | Outbound headers (auth values redacted). |
request_body | object | The JSON payload sent. |
response_status | int | null | HTTP status received. |
response_headers | object | null | Headers from the target (capped to 4 KiB). |
response_body | string | null | Body from the target (capped to 16 KiB). |
error | string | null | Transport error (timeout, DNS failure, TLS). |
duration_ms | int | null | Wall time of the HTTP call. |
delivered_at | datetime | null | When the call finished. |
next_retry_at | datetime | null | When the next retry is scheduled (if any). |
created_at / updated_at | datetime |
Status
| Status | Meaning |
|---|---|
pending | Queued or in-flight; delivered_at not set. |
success | 2xx received within the timeout. |
failed | Non-2xx, timeout or transport error. May retry — check next_retry_at. |
Example
{
"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"
}
Reading
| Method | Path |
|---|---|
GET | /webhooks/{id}/deliveries?status=failed&attempt[gte]=2 |
GET | /webhooks/{id}/deliveries/{deliveryId} |
Filters: status, attempt[gte|lt], created_at[gte|lt].
Replay
POST /webhooks/{id}/deliveries/{deliveryId}/replay enqueues an immediate redelivery — it ignores any pending backoff but is recorded as a new row (attempt is not incremented; a new WebhookDelivery is appended with attempt=1 and a metadata.replay_of reference).
Once any delivery of an event to a webhook has succeeded — a replay included — the retries still scheduled for that event are not sent, and the next_retry_at of its earlier failed rows is cleared. A successful replay therefore ends the backoff chain of the original delivery.
The replayed request also carries its own X-Idempotency-Key, a fresh random value rather than the event id every automatic attempt uses (see Headers). Automatic retries are still deduplicated against the original attempt on purpose; a replay is a deliberate request to act on the event again, so it must not be swallowed by the same guard.