Retries and replay
Inspect delivery history, tell receiver failures from provider failures, and replay an event without changing its identity.
History and replay are server operations on the delivery instance in the endpoint's scope. They are not exposed as Better Auth routes.
Retry policy
| Response | Outcome |
|---|---|
2xx | Succeeded |
Network error, 408, 425, 429, 5xx | Retried on the schedule |
Any other 4xx | Failed, no retry |
3xx | Failed. Redirects are never followed |
Defaults: delays of 1s, 5s, 30s, 2m, 10m, and 30m (seven attempts in total), and a maximum delivery age of 24 hours. Configure both through retry on the factory. A retry keeps the event ID and body and gets a fresh timestamp and signature.
Two things look like failures but are not receiver failures:
- Paused endpoint. The delivery waits until resume or expiry.
- Provider lookup failure. The delivery is deferred without consuming an attempt, and the error goes to the worker's
onError.
Inspect deliveries
History and replay work on the client for the endpoint's scope:
import { webhooks } from './webhooks.js'
const customer = webhooks.forScope({ type: 'organization', id: organizationId })
const page = await customer.deliveries.list({ eventId: publication.eventId, limit: 25 })
for (const item of page.items) {
const detail = await customer.deliveries.get(item.id)
console.log(detail.status, detail.lastStatus, detail.attempts.length)
}Filter by eventId, endpointId, or status. Results are newest first; pass page.nextCursor as before for the next page. get() includes the attempt list with status codes, captured response bodies (up to maxResponseBytes), and errors.
Response bodies and payloads can contain customer data. Treat history as sensitive.
Replay
- youFix the cause
The receiver is back, or its bug is fixed.
- your appreplay()
Creates a new delivery of the same event.
- workerSend
Same event ID and body, fresh retry budget.
- receiverDedupe
Already processed? The receiver skips it.
After fixing the cause, replay the original delivery:
const detail = await customer.deliveries.get(deliveryId)
if (detail.replayOf === null && ['succeeded', 'failed'].includes(detail.status)) {
const replay = await customer.deliveries.replay(detail.id)
}Rules:
- Only original
succeededorfaileddeliveries are eligible. A replay cannot be replayed. - One pending or in-flight replay per original at a time, otherwise
REPLAY_IN_PROGRESS. - The endpoint must be active and the event still retained.
- The replay gets a new delivery ID and a fresh retry budget, the same event ID and body, and the endpoint's current URL and secrets.
- The worker still has to run.
Because the event ID is unchanged, a deduplicating receiver ignores a replay of an event it already processed. That is the intended way to replay safely.