Skip to main content

Deliveries and retry

No delivery is "fire and forget". Each event × endpoint becomes a delivery record with status, attempts and the full capture of request and response — which turns "the webhook never arrived" from a mystery into a lookup.

Deliveries

The lifecycle of a delivery

StatusMeaning
pendingScheduled, awaiting processing
successYour endpoint responded 2xx
failedFailed; retry scheduled
exhaustedRan out of attempts (or permanent failure); went to the DLQ

What counts as a failure: a non-2xx response (redirects are not followed, so 3xx also fails), a network error and a timeout (10 seconds per attempt).

Your endpoint's contract

Three rules for a healthy consumer:

  1. Respond 2xx immediately, without processing synchronously. Persist the payload (queue, table, log) and return 200 right away; process later. The timeout is 10 seconds — a handler that queries the database, calls third parties and only then responds will blow past the limit on a busy day and trigger unnecessary retries.
  2. Unknown event? Respond 200 and ignore it. New events may start being emitted without notice (an endpoint with an empty event list receives everything). Returning an error for an event you don't handle only generates retries and noise in your queue.
  3. Deduplicate before processing — see "at-least-once" below.

Retry: 8 attempts over ~8h30 (exponential backoff)

Each delivery gets 8 attempts with exponential backoff at a 4-minute base — the wait doubles on each failure:

AttemptWhen (approx.)Wait after the previous failure
1stimmediate
2nd+4min4min
3rd+12min8min
4th+28min16min
5th+1h32min
6th+2h041h04
7th+4h122h08
8th+8h284h16

The full window is ~8h30 between the event and the last attempt (times are approximate: they depend on the queue at the moment) — a consumer that is down for minutes or hours still receives the event without manual intervention. The screen shows the estimated next attempt (nextRetryAt). Each attempt is re-signed with a fresh t in the signature.

Two situations skip the retry and go straight to exhausted:

  • SSRF block: the URL started resolving to an internal network — a permanent failure; rescheduling would not help.
  • Endpoint inactive or removed at delivery time.

DLQ: nothing vanishes silently

When the 8th attempt fails, the delivery becomes exhausted and the job goes to the dead-letter queue with the error recorded. The event itself remains persisted and immutable; the exhausted delivery stays listed on the screen, ready for manual redelivery once you fix the endpoint.

The deliveries screen

On the endpoint's row, open the history (10 per page). Each delivery shows event, status, HTTP code, number of attempts and date; expanding the row reveals:

  • the error of the last attempt,
  • the request body sent,
  • the response body your server returned,
  • and the moment of successful delivery (deliveredAt).

Sensitive headers (signature, authentication credentials, the response's Set-Cookie) appear as [REDACTED]. The endpoint listing aggregates total deliveries and highlights in red the sum of failed + exhausted.

Manual redelivery

failed or exhausted deliveries have a reprocess button (also via POST /api/v1/webhook-endpoints/{id}/deliveries/{deliveryId}/retry). Redelivery resets the delivery and restarts the attempt cycle; if the endpoint is inactive, the request is refused (409 ENDPOINT_INACTIVE). Each redelivery goes into the audit trail.

At-least-once delivery

The system guarantees the event is not lost, not that it arrives exactly once: a 200 lost on the network triggers a new attempt and, therefore, a duplicate delivery on your side. Design the consumer to be idempotent: deduplicate by the X-Webhook-Event-Id header (the event's UUID, stable across attempts and redeliveries) before processing. The X-Webhook-Attempt header carries the attempt number, useful for telemetry on your side.

Testing without a business event

POST /api/v1/webhook-endpoints/{id}/test fires a webhook.test event through the real pipeline — the delivery shows up on the screen like any other, with capture and retry. Use it to validate signature, auth and the consumer contract before going to production.