Delivery & retries
Delivery & retries
At-least-once delivery, deduplication, the retry ladder, and the dead-letter queue.
At-least-once, so dedupe
Delivery is at-least-once. A delivery that stalls mid-flight is reclaimed after 15
minutes and re-sent, so you may occasionally receive the same event twice — dedupe on
Drazill-Webhook-Id (idempotent by construction). Record the delivery id the first time
you durably accept an event, and ignore a repeat.
A re-sent delivery is signed fresh, so its Drazill-Webhook-Timestamp and
Drazill-Webhook-Signature differ from the first attempt while Drazill-Webhook-Id stays
the same. Dedupe on the id, never on the signature.
No ordering guarantee
Deliveries are claimed globally by when they are next due, not per endpoint and not per
entity, so events can arrive out of order — an order.updated may reach you before the
order.created it followed, especially after a retry has pushed one event down the ladder
while newer ones flow normally.
Do not reconstruct state from the event stream’s order. Every event carries the ids you need to re-read authoritative state over REST; when order matters, that read is the answer.
Retries and the dead-letter queue
Respond 2xx once you have durably accepted the event. Any non-2xx response or a timeout
is a failed attempt. Failed attempts back off on this schedule (seconds), then move to the
dead-letter queue:
That is seven attempts over roughly a day. A payload that fails its own schema is never sent — it fails closed into this same retry/DLQ path rather than delivering unvalidated data.
Inspect and replay
Inspect deliveries and replay one from the API:
A replay is a new delivery of the same event: it gets a new
Drazill-Webhook-Id and re-enters the ladder at attempt 1, while the envelope’s id
(the event id) is unchanged. Replays carry replayed_from_delivery_id pointing at the
original, and are excluded from your endpoint’s health rates.
Recovering a whole outage
After a receiver has been down long enough to dead-letter a batch, replay the batch in one call instead of one call per row:
since is required — there is no unbounded form of this call — and is clamped to the
dead-letter retention window below. limit caps at 500 per call; remaining tells you
whether to call again. Already-replayed deliveries are skipped, so calling twice does not
duplicate anything.
Order of operations if the endpoint is suspended: reactivate first, then replay. Bulk
replay returns 409 on a suspended endpoint (asking you to reactivate) and on a disabled
one (whose queue was already cancelled).
How long deliveries are kept
Delivery history is not retained forever — partly for storage, partly because every attempt row stores up to 4 KB of your receiver’s raw response body.
The dead-letter window is the replay window: a dead-lettered delivery can only be replayed while it still exists, which is why it is kept three times as long as a settled one.
Test deliveries bypass your subscriptions
POST /webhooks/{id}/test and POST /webhooks/sandbox/events deliver to the endpoint you
name whether or not it subscribes to that event type. That is deliberate — you are
testing reachability and your verification code, not your subscription filter — but it
means a test delivery is not proof that the same event would reach you in production.
Check subscribed_events for that.
Both return 409 WEBHOOKS_DISABLED if webhook delivery is turned off platform-wide, rather
than silently succeeding with nothing queued.
Test your integration
POST /webhooks/{id}/test— sends a real, signedwebhook.testdelivery to one endpoint.POST /webhooks/sandbox/events— enqueues any event type with a sample or your owndata(validated), for exercising your handler without live activity.
Both go through the exact signing + delivery path production uses, so verifying one proves
your verification code. Test and live deliveries are segregated by livemode, so a sandbox
event never reaches a live endpoint.
Return 2xx fast and do the real work asynchronously. If you can’t finish within the
delivery timeout, accept the event (dedupe on Drazill-Webhook-Id) and process it out of
band, rather than holding the connection open.

