What is webhook replay? Redelivery vs replay attacks

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 200 without 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:

SourceHow
The providerGitHub has a Redeliver button per delivery. Stripe lets you resend events from the dashboard. Many providers have nothing.
A request log or gatewayAny tool that stored the original request can resend it.
Your own storeIf your endpoint saved the raw payload before processing, reprocess from there.
The provider's APINo 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 relay agent is connected are stored as received. 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, recover re-sends deliveries that ran out of retries and replay-missing sends deliveries skipped while an endpoint was paused or disabled, within the 35-day history. Every copy keeps the original webhook-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:

  1. 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.

  1. Deduplicate on the event ID. A replay inside the tolerance window still carries an ID you've already processed. See idempotency.
  2. 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 429 or 503. Limit the batch size or throttle delivery.

Frequently asked questions

What is webhook replay?

Webhook replay means sending a webhook that was already sent before, again. Deliberate replay, also called redelivery, is a recovery tool for re-sending events your endpoint missed or failed to process. A replay attack is the malicious version, in which someone captures a valid signed webhook and sends it again to trigger the same action.

How do I replay a webhook?

Use the provider's redelivery option if it has one, such as the Redeliver button on a GitHub webhook delivery or resending an event from the Stripe dashboard. If webhooks pass through a gateway or request log that stored them, resend them from there. Without a stored copy, a missed webhook can only be recovered by fetching the current state from the provider's API.

How do I prevent webhook replay attacks?

Verify a signature that covers a timestamp, and reject requests whose timestamp is more than a few minutes old. Stripe, Slack and Standard Webhooks all sign a timestamp for this reason. Also deduplicate on the event ID, so a replay inside the tolerance window has no effect.

Is replaying a webhook safe?

It is safe if your handler is idempotent. A replayed webhook usually carries the same event ID as the original, so a handler that records processed IDs will skip events it already handled and process only the ones it missed.