# Replaying & retries (/en/integrate/webhooks/replaying-and-retries)

How XPay automatically retries failed deliveries, and how to manually resend an event from the Workbench.

A webhook delivery can reach your endpoint more than once for two distinct reasons. XPay **automatically retries** when your handler returns non-2xx or times out. You can **manually replay** a delivery yourself from the Workbench when you need to fix a handler bug and re-feed the event. Both arrive as ordinary POSTs your verifier already handles; the difference is who initiated them.

## Automatic retries [#automatic-retries]

A delivery is "failed" if any of these happen:

* Your handler returns a non-2xx HTTP response.
* Your handler doesn't respond within **30 seconds** per attempt.
* The TCP connection fails (DNS, refused, reset, TLS error).

When an attempt fails, XPay schedules the next one and continues until it succeeds or runs out of attempts. The schedule depends on the mode the event was created in. Live mode retries over a much longer window than [Test mode](/get-started/test-mode), so a customer-facing endpoint that's briefly down still receives the event once it recovers.

Live mode retries 13 times over roughly 3 days. The gap between attempts grows as failures persist.

| Attempt | Fires after the previous attempt by |
| ------- | ----------------------------------- |
| 1       | immediately                         |
| 2       | 1 minute                            |
| 3       | 5 minutes                           |
| 4       | 30 minutes                          |
| 5       | 1 hour                              |
| 6       | 2 hours                             |
| 7       | 4 hours                             |
| 8       | 8 hours                             |
| 9 to 13 | 12 hours each                       |

Test mode retries 5 times over roughly 2 hours 35 minutes, so you see the full failure path quickly while testing.

| Attempt | Fires after the previous attempt by |
| ------- | ----------------------------------- |
| 1       | immediately                         |
| 2       | 1 minute                            |
| 3       | 5 minutes                           |
| 4       | 30 minutes                          |
| 5       | 2 hours                             |

A delivery succeeds on the first 2xx response. As soon as one attempt returns 2xx, the delivery is `succeeded` and no further retries happen, even if you later return an error to a duplicate.

### When XPay gives up [#when-xpay-gives-up]

After the final attempt fails (the thirteenth in Live mode, the fifth in Test mode), the delivery's status is `failed`.

When a delivery exhausts all retries on an otherwise-healthy endpoint, XPay emails your account's owners, admins, and developers in Live mode so you can fix or replay it. The email names the endpoint URL and the event type. (Test mode deliveries never trigger emails.)

If the endpoint has had no successful delivery at all while that event was failing, XPay treats it as down: it disables the endpoint and stops sending it new events. This applies in both Test and Live mode. A single event that keeps failing won't disable an endpoint that's otherwise delivering fine, only a fully dark one. When XPay disables a Live mode endpoint it always emails you, even if you've muted the delivery-failure alert. A disabled endpoint shows a `Disabled` badge in the dashboard; re-enable it once your handler is fixed.

A failed delivery isn't lost. The Workbench's Events tab keeps deliveries for 90 days and you can replay any of them once your handler is fixed.

## Manual replay [#manual-replay]

The Workbench's **Events** tab shows every event your account has produced in the last 90 days, along with each delivery to each endpoint and every attempt within it. On any delivery row there's a **Resend** button that POSTs the same JSON payload to the same endpoint a fresh time.

Reach for replay when:

* You shipped a bug, your handler dropped a real event, and you need to re-feed it now that the bug is fixed.
* You're debugging a new handler against historical data.
* You added a new event type to your subscription and want to backfill recent events into the new handler.

Replays are not a substitute for fixing the underlying handler. If your handler still 500s, replay just produces another failed delivery.

### What's different from a retry [#whats-different-from-a-retry]

A manual replay is fire-and-forget: &#x2A;*single attempt, no retry schedule.** If your endpoint returns non-2xx to a replay, XPay does not retry it. Click **Resend** again from the Workbench when you're ready for another shot.

Replays also don't update the original delivery's status. They create a new sibling delivery alongside the original, and the Events tab groups them together by URL so you can see the whole timeline against one endpoint at once.

For the click-by-click walkthrough of the Events tab (filtering, drilling into attempts, reading request and response bodies), see [Workbench → Events panel](/integrate/workbench/events-panel).

## What replays look like on your end [#what-replays-look-like-on-your-end]

Your handler can't easily tell a replay from an original or from an automatic retry. They all arrive as POSTs to the same URL with the same JSON body.

What stays the same on every retry or replay of a single event:

* `event.id`, the unique event ID.
* `event.type`, the kind of event it is.
* `event.data.object`, the resource payload.
* The raw bytes of the JSON body.

What changes on every attempt or replay:

* The `t` value in `XPay-Signature` is the timestamp of **this** delivery attempt, not the original event.
* The `v1` signature in `XPay-Signature` is recomputed against that fresh `t` value.

Two consequences for your verifier:

1. **The replay window is per-attempt.** A replay arriving 3 days after the original event still has a `t` value within seconds of "now," so the `abs(now - t) > 300` check passes. Don't compare `t` against the resource's `created` timestamp; they diverge by design.
2. **Signature verification works the same.** Recompute against the fresh `t` and the raw body using your endpoint's signing secret. See [Verifying signatures](/integrate/webhooks/verifying-signatures).

## Build idempotency on `event.id` [#build-idempotency-on-eventid]

Because every retry and every replay carries the same `event.id`, the right key for "have I already handled this?" is `event.id`, not the timestamp or any field on the resource.

```typescript
async function handleEvent(event: { id: string; type: string; data: unknown }) {
  // First-write-wins on event.id
  const inserted = await db.processedEvents.insertIfNew(event.id);
  if (!inserted) return; // already processed; no-op

  // Real work happens here
}
```

`insertIfNew` is one row in a table of processed event IDs (or a Redis `SET NX` if you prefer). The work below it runs at most once per event, no matter how many times XPay retries or you replay.

For the full pattern (verify, dedup, ack fast, work in a queue), see [Verifying signatures → Idempotency](/integrate/webhooks/verifying-signatures#idempotency).

## Where to next [#where-to-next]

<Cards>
  <Card icon="<ListTree />" title="Workbench: Events panel" href="/integrate/workbench/events-panel">
    The full walkthrough of the Events tab. Filter, inspect attempts, click **Resend** on a
    delivery.
  </Card>

  <Card icon="<ShieldCheck />" title="Verifying signatures" href="/integrate/webhooks/verifying-signatures">
    Recompute the signature on every retry and replay. Idempotency on `event.id`.
  </Card>

  <Card icon="<Cable />" title="Setting up an endpoint" href="/integrate/webhooks/setting-up-an-endpoint">
    Add an endpoint, pick events, copy the signing secret. XPay emails you if an endpoint keeps
    failing.
  </Card>

  <Card icon="<Terminal />" title="Local development" href="/integrate/webhooks/local-development">
    Tunnel deliveries to your laptop while you debug.
  </Card>

  <Card icon="<Boxes />" title="Event reference" href="/integrate/webhooks/event-reference">
    Every event you can subscribe to, when it fires, and the object it carries.
  </Card>
</Cards>