Webhook replay means sending a webhook that was already sent once, again. The term has two meanings. Deliberate replay (redelivery) is a recovery tool: re-sending events your endpoint missed during an outage or failed to process because of a bug. A replay attack is the malicious version: someone captures a valid, correctly signed webhook and sends it again to repeat its effect. A good webhook setup supports the first and blocks the second.
Deliberate replay: recovering missed webhooks
Typical reasons to replay:
- Your endpoint was down during a deploy and the provider's retries ran out.
- A bug made your handler return
200without doing the work. You fixed it and need to reprocess the affected events. - You added a new destination and want to backfill it with recent events.
- You're debugging and want to send the exact same request to a local build.
Where you can replay from:
| Source | How |
|---|---|
| The provider | GitHub has a Redeliver button per delivery. Stripe lets you resend events from the dashboard. Many providers have nothing. |
| A request log or gateway | Any tool that stored the original request can resend it. |
| Your own store | If your endpoint saved the raw payload before processing, reprocess from there. |
| The provider's API | No replay at all: list objects changed since the outage and reconcile. |
Replay in Webhook Relay
Because Webhook Relay stores each request before delivering it, you can replay without involving the provider:
- From the logs. Resend any single delivery, recover failed deliveries in bulk after an endpoint comes back, or replay missing events to backfill a destination.
- On agent connect. For destinations on localhost or a private network, webhooks that arrive while no
relayagent is connected are stored asreceived. With replay on agent connect enabled, they're sent oldest-first as soon as the agent reconnects. You choose the lookback window (8 hours by default, up to 7 days) and the maximum number considered (100 by default, up to 5,000). - Outbound webhooks. In the Outbound Gateway,
recoverre-sends deliveries that ran out of retries andreplay-missingsends deliveries skipped while an endpoint was paused or disabled, within the 35-day history. Every copy keeps the originalwebhook-id. See monitor, retry and recover deliveries.
Replay attacks
If an attacker captures a signed webhook, from a log, a proxy, or a misconfigured request bin, the signature is still valid. Sending it again could, for example, mark an order paid twice.
Defences:
- Sign a timestamp and check it. If the signed string includes the time, the attacker can't change it without breaking the signature. Reject anything older than a few minutes:
const age = Math.abs(Date.now() / 1000 - Number(headers['webhook-timestamp']))
if (age > 300) return res.sendStatus(401) // outside the 5-minute tolerance
Stripe (t= in Stripe-Signature), Slack (X-Slack-Request-Timestamp) and Standard Webhooks (webhook-timestamp) all sign a timestamp. GitHub doesn't, so rely on the delivery ID there.
- Deduplicate on the event ID. A replay inside the tolerance window still carries an ID you've already processed. See idempotency.
- Keep your clocks synced. Timestamp checks need NTP on the receiving server, or legitimate requests start failing.
Making deliberate replay work with timestamp checks
There's a tension: a replay of a week-old webhook fails a 5-minute timestamp check. Good senders re-sign on every attempt. Standard Webhooks senders, including the Webhook Relay Outbound Gateway, give each retry a new timestamp and signature while keeping the same webhook-id, so the receiver accepts it and still recognises it as the same event. If you replay a captured request yourself to a local build, either turn off the timestamp check in development or re-sign the request with the signature generator.
Common mistakes
- Non-idempotent handlers. Replaying a batch that includes events you already processed then repeats their side effects.
- No stored copy. If nothing kept the raw request, there's nothing to replay. Store payloads, or put a gateway in front that does.
- Replaying out of order. Replay oldest-first, and make handlers compare timestamps rather than trusting arrival order.
- Dumping a large backlog at full speed. A replay of thousands of events can overload the endpoint and trigger
429or503. Limit the batch size or throttle delivery.
