# Replay deliveries

> Send an event to your webhook again, signed afresh, after you've fixed whatever made it fail. One delivery from the dashboard or the API, or every delivery that gave up with a short script.

A replay sends an event to the same webhook again, as a new delivery. Use it after you've fixed a bug or brought a server back: the events that failed in the meantime arrive as if for the first time, and your handler deals with them like any other.

Ravi's appointment app was down for four days while he moved hosts. BlueBot retried every reply his clients sent for 72 hours, then gave up on them. Once the new host answered, Ravi listed the deliveries that gave up and replayed them, and his app caught up on 37 replies without anyone texting twice.

## What a replay does

- It makes a new delivery of the same event to the same webhook, and its `replay_of` names the delivery it repeats.
- The event is unchanged: the same `id`, the same `webhook-id` header and the same body, with `data` as it was when the event happened.
- It's signed afresh with the webhook's current secret and a new `webhook-timestamp`, so it passes your signature check even weeks later.
- It goes out within a minute, and if it fails it's retried on the [usual schedule](https://usebluebot.com/docs/webhooks/retries#the-retry-schedule).
- Any delivery from the last 30 days can be replayed, whatever its state: one that gave up, one still retrying, or one that was delivered.
- A replay to a paused webhook waits until you resume the webhook.

> **Replays and your duplicate check.** A replay carries the same `webhook-id` as the original. If your handler skips ids it has already handled, as it should, a replay of an event you finished is skipped too. That's what you want after an outage. To run an event through your handler a second time on purpose, take its id out of your store first.

## Replay one delivery

In the dashboard, open the webhook in **Developers → Webhooks**, find the delivery in its list, and press **Send again**.

With the API, find the delivery's id in the [deliveries log](https://usebluebot.com/docs/webhooks/retries#the-deliveries-log), then:

```bash
curl -X POST "https://usebluebot.com/api/v1/webhooks/wh_1Hq2LxW9cVbN3mKpRtYs0a/deliveries/dlv_1Hq2LxW9cVbN3mKpRtYs0a/replay" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Idempotency-Key: replay-dlv_1Hq2LxW9cVbN3mKpRtYs0a"
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const delivery = await client.webhooks.deliveries.replay('wh_1Hq2LxW9cVbN3mKpRtYs0a', 'dlv_1Hq2LxW9cVbN3mKpRtYs0a', { idempotencyKey: 'replay-dlv_1Hq2LxW9cVbN3mKpRtYs0a' });
console.log(delivery);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

delivery = client.webhooks.deliveries.replay(
    "wh_1Hq2LxW9cVbN3mKpRtYs0a",
    "dlv_1Hq2LxW9cVbN3mKpRtYs0a",
    idempotency_key="replay-dlv_1Hq2LxW9cVbN3mKpRtYs0a",
)
print(delivery)
```

The answer, `202`:

```json
{
  "id": "dlv_2Bf8QzD4kJmA6nXwEuTr1c",
  "object": "webhook_delivery",
  "webhook_id": "wh_1Hq2LxW9cVbN3mKpRtYs0a",
  "event_id": "evt_1Hq2LxW9cVbN3mKpRtYs0a",
  "event_type": "message.received",
  "test": false,
  "state": "pending",
  "attempts": 0,
  "last_status": null,
  "last_error": null,
  "last_duration_ms": null,
  "next_attempt_at": "2026-11-13T19:41:00.000Z",
  "delivered_at": null,
  "replay_of": "dlv_1Hq2LxW9cVbN3mKpRtYs0a",
  "created_at": "2026-11-13T19:41:00.000Z"
}
```

The answer is the new delivery, waiting to go. Read it again with the deliveries list to see how it went.

| You got                       | It means                                                                      | Do next                                                                           |
| ----------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `202` with `state: "pending"` | The replay is queued and goes within a minute.                                | Watch your handler's logs, or list the webhook's deliveries and look for this id. |
| `404 not_found`               | The delivery isn't on that webhook, or it's older than 30 days.               | Check both ids. For an older event, there's nothing left to send.                 |
| `409 idempotency_conflict`    | You used this `Idempotency-Key` for a different request in the last 24 hours. | Use one key per delivery, such as `replay-<delivery id>`.                         |

The `Idempotency-Key` matters here: without one, a script that runs twice replays every delivery twice.

## Replay everything that gave up

After an outage, list the webhook's deliveries that gave up and replay each one. The SDKs page through the list for you, and the key per delivery means running the script again within 24 hours replays nothing twice.

```ts title="TypeScript"
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });
const webhookId = 'wh_1Hq2LxW9cVbN3mKpRtYs0a';

let count = 0;
for await (const delivery of client.webhooks.deliveries.list(webhookId, { state: 'gave_up' })) {
  await client.webhooks.deliveries.replay(webhookId, delivery.id, { idempotencyKey: `replay-${delivery.id}` });
  count += 1;
}
console.log(`Replayed ${count} deliveries.`);
```

```python title="Python"
import os

from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])
webhook_id = "wh_1Hq2LxW9cVbN3mKpRtYs0a"

count = 0
for delivery in client.webhooks.deliveries.list(webhook_id, state="gave_up"):
    client.webhooks.deliveries.replay(webhook_id, delivery.id, idempotency_key=f"replay-{delivery.id}")
    count += 1
print(f"Replayed {count} deliveries.")
```

Each replay is one request, and a key may make 120 a minute. The SDKs wait and try again when BlueBot answers `429 rate_limited`, so a long list takes a few minutes. [Rate limits](https://usebluebot.com/docs/concepts/rate-limits)

Replays you send together arrive close together and not always in the order the events happened. If your handler cares about order, see [Order isn't guaranteed](https://usebluebot.com/docs/webhooks/retries#order-isnt-guaranteed).

## When a replay isn't the right tool

- **The event is older than 30 days.** Deliveries and events are kept 30 days, so there's nothing left to replay.
- **You want the events at a different address.** A replay only goes to the webhook it was made for. Read the events with `GET /v1/events` and post them where you need them, or point a new webhook at the new address for what happens from now on.
- **You want today's state, not the event's.** `data` is the record as it was. To act on a text's state now, read the text: `GET /v1/messages/{id}`.

## Next

- [Retries and the deliveries log](https://usebluebot.com/docs/webhooks/retries): When BlueBot retries, and how to read every attempt.
- [Rotate secrets](https://usebluebot.com/docs/webhooks/rotate-secrets): Change a webhook's secret without missing a delivery.
