API Reference
API Reference
GitHub
Webhooks and Events

Delivery Tracking

Track webhook delivery history

Every attempt against a webhook is recorded, successes included. That log is the only place a failure shows up — webhooks fail quietly by design, since a receiver being down is not a reason to fail the send that triggered the event.

GET /api/v1/webhooks/{id}/deliveries?limit=50

The log belongs to one webhook, so its id is in the path.

curl "http://localhost:3000/api/v1/webhooks/0198f6a1-3c7e-7b21-9f4d-2a5c8e0b1d33/deliveries?limit=50" \
  -H "Authorization: Bearer myk_..."
{
  "deliveries": [
    {
      "id": "0198f6b2-1a44-7c90-8e2f-3d5a7b9c1e04",
      "webhook_id": "0198f6a1-3c7e-7b21-9f4d-2a5c8e0b1d33",
      "project_id": "0198f6a0-9d12-7f33-a1b8-6c4e2f8a0d57",
      "event": "email.failed",
      "status": "failed",
      "http_status": 500,
      "error_message": "HTTP 500",
      "attempt": 3,
      "created_at": "2026-01-01T00:01:00Z"
    }
  ],
  "next_cursor": "MjAyNi0wMS0wMVQwMDowMTowMFowMTk4ZjZiMi0xYTQ0"
}

Reading a row

FieldNotes
eventWhich event triggered this delivery
statussuccess or failed — nothing else
http_statusWhat your endpoint answered. Absent when the request never got a response
error_messageWhy it failed: an HTTP status, a timeout, a dial error
attemptWhich attempt this row is, counting from 1

One row per attempt. A message that took three tries leaves three rows, so a webhook that eventually succeeded looks like two failures followed by a success — read attempt alongside status before concluding anything is broken.

http_status being absent is the interesting case: it means no HTTP response came back at all. A timeout, a refused connection, a TLS failure, or a target the SSRF guard would not dial. error_message says which.

Paging

Keyset, like every log that grows on its own. Pass next_cursor straight back as cursor and keep going until it comes back empty:

curl -G "http://localhost:3000/api/v1/webhooks/$ID/deliveries" \
  -H "Authorization: Bearer myk_..." \
  --data-urlencode "limit=50" \
  --data-urlencode "cursor=$NEXT_CURSOR"

Treat the cursor as opaque. It is base64 and it encodes a timestamp and a row id, but nothing promises that shape — and a cursor it cannot parse is answered with the first page rather than an error, on the grounds that a stale browser tab should not fail a list request.

There is no total. The cursor coming back empty is how you know you have reached the end.

Retry policy

SettingDefaultDoes
webhook.max_attempts3Total attempts including the first, so the default is one delivery and two retries
webhook.retry_delay10sA fixed wait between attempts, not exponential backoff
webhook.timeout10sBounds each individual request

Any 2xx counts as success. Everything else is retried until the attempts run out, at which point the event is dropped and logged — there is no dead-letter queue to drain later.

The defaults give you thirty seconds, total

Three attempts, ten seconds apart, each bounded at ten seconds. A receiver that is down for a minute loses the event permanently.

If your endpoint can be briefly unavailable, answer 2xx as soon as you have durably accepted the payload and do the work afterwards. Treat the webhook as a nudge rather than as delivery of the data itself, and reconcile against the email log for anything that must not be missed.

What is not attempted

A delivery is skipped without a log row when the webhook is not subscribed to the event, or when the sender does not pass the webhook’s filters . Those are not failures, so nothing is recorded — an empty log for a webhook that should be firing usually means one of the two, not a broken receiver.

Retention

Delivery rows are removed by the webhook_retention_days platform setting . Zero keeps them forever, which on a busy install is a table that grows faster than the email log — one row per attempt per subscribed webhook per event.

This list may be served by a read replica

Where replicas are configured, delivery history is one of the query groups allowed to run on one (database.replica_reads.webhook_deliveries, on by default). A delivery that happened a moment ago can be missing for as long as replication lag lasts.