# Retries and replay

> Inspect delivery history, tell receiver failures from provider failures, and replay an event without changing its identity.

Source: https://good-webhooks.dev/docs/operations/retries-and-replay



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 [#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 [#inspect-deliveries]

History and replay work on the client for the endpoint's scope:

```ts
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 [#replay]

1. **Fix the cause** (you): The receiver is back, or its bug is fixed.
2. **replay()** (your app): Creates a new delivery of the same event.
3. **Send** (worker): Same event ID and body, fresh retry budget.
4. **Dedupe** (receiver): Already processed? The receiver skips it.

After fixing the cause, replay the original delivery:

```ts
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 `succeeded` or `failed` deliveries 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](https://good-webhooks.dev/docs/operations/retention.md).
* 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](https://good-webhooks.dev/docs/guides/receive.md#process-each-event-once) ignores a replay of an event it already processed. That is the intended way to replay safely.
