Launch special — let's split the check with SPLITCHECK for 50% off
9 min read

Webhook Replay: The Definitive Guide to Replaying and Resending Webhooks

How to replay webhooks: resend captured payloads to any URL, fix handler bugs, reproduce incidents locally, and handle signatures and idempotency safely.

ReplayDebuggingWebhooks
O

Ozer

Developer & Founder of HookSense

Webhook replay is the act of re-sending a previously captured webhook — the exact headers, body, and query string the provider originally delivered — to an endpoint of your choosing. It is the single most useful tool for debugging webhook handlers, because it turns a one-shot, fire-and-forget event into something you can run again and again: against localhost with a debugger attached, against staging with an edited payload, or against production to backfill events your server missed during an outage.

This guide covers what replay actually is (and how it differs from a provider's "resend" button), the four situations where it saves the most time, the two gotchas that bite everyone — idempotency and signature verification — and a practical workflow for replaying webhooks straight to your local machine.

Replay vs. Resend: They Are Not the Same Thing

The terms get used interchangeably, but there is a real difference:

  • Provider resend — Stripe, GitHub, Shopify, and most major providers have a dashboard button to re-deliver an event. The provider generates a brand-new request, with a fresh timestamp and a fresh signature, and sends it to the endpoint URL registered in their dashboard. You cannot change the destination, you cannot edit the payload, and in many dashboards you can only resend recent events.
  • Webhook replay — you (or your tooling) keep a captured copy of the original request and re-send that copy yourself. Because you own the copy, you control everything: the destination URL, the method, the headers, the body. You can point it at localhost, mutate one field to test an edge case, or fire it fifty times in a row.

Provider resend is great when the original delivery failed and you just want the provider to try again. Replay is what you want for everything else — debugging, testing, and incident recovery — because the destination and payload are under your control. HookSense captures every request that hits your endpoint (headers, body, and query string) and lets you replay any captured request to any URL, with optional edits to the method, headers, or body before sending.

The Four Use Cases Where Replay Pays for Itself

1. You Fixed the Handler Bug — Now Re-Run the Exact Payload

This is the classic loop. A webhook arrives, your handler throws, you read the stack trace, you patch the code. Now what? Without replay, you are stuck triggering the event again from the provider's side — make another test purchase, push another commit, cancel another subscription. That is slow, and worse, the new event is not the same event — the payload that broke your handler had something specific in it: a null field, a unicode customer name, an oversized line-item array.

With replay, you take the captured request that caused the failure and send it back at your fixed handler. Same bytes, same headers, same edge case. If it passes now, you know the fix addresses the actual failure, not a lookalike.

2. Testing Edge Cases by Editing a Real Payload

Hand-writing webhook fixtures is tedious and error-prone — real provider payloads are deeply nested and full of fields your mock never includes. A better approach: capture one real event, then edit it before replaying. Change the amount to zero. Remove the email field. Swap the event type. Set the quantity to a negative number. Each variant is one edit away from a payload you know is structurally authentic, because it started life as a real delivery.

HookSense lets you modify the method, headers, and body of a captured request before replaying it, so a single real Stripe event can become a whole suite of edge-case tests.

3. Reproducing a Production Incident Locally

Something failed in production at 3:14 AM. The logs say a webhook handler returned 500, but logs rarely capture the full request. If your inspection tool was in the delivery path, the complete request is sitting in your history. Pull it up, replay it to localhost, attach a debugger, and step through the failure with the actual production payload. No guessing what the body looked like, no reconstructing headers from log fragments.

4. Backfilling After Downtime

Your server was down for forty minutes during a deploy gone wrong. The provider kept sending webhooks; some providers retry, some do not, and retry windows eventually expire. (For a breakdown of how different providers handle this, see our guide to webhook retry strategies.) If every inbound request was captured, recovery is mechanical: filter the capture history to the outage window, replay each request to the now-healthy endpoint, and verify the responses. Forty minutes of missed events becomes a ten-minute replay session instead of a support ticket to the provider.

The Idempotency Requirement: Replays Are Duplicates

Here is the rule that makes everything above safe: a replayed webhook is a duplicate delivery, so your handler must be idempotent before you replay against anything that matters.

Idempotency means processing the same event twice produces the same result as processing it once. If your handler charges a card, sends an email, or increments a counter without first checking whether it has already seen this event, every replay will repeat that side effect. Double charges and duplicate emails are exactly the kind of bug that turns a debugging session into an apology email.

The standard pattern is cheap insurance:

// Deduplicate by the provider's event ID before doing work
async function handleWebhook(event) {
  const seen = await db.events.findOne({ eventId: event.id });
  if (seen) {
    return { status: 200, body: 'already processed' };
  }
  await db.events.insert({ eventId: event.id, processedAt: new Date() });
  await processEvent(event); // safe to run business logic now
}

Note that this is not just a replay concern — providers themselves deliver duplicates during retries, so an idempotent handler is table stakes for any production webhook integration. We cover the patterns in depth in webhook idempotency: why and how. Once your handler deduplicates correctly, replay becomes a zero-risk operation: you can fire the same event at production ten times and the ledger will not move.

The Signature Caveat: Old Signatures Get Rejected (On Purpose)

The second gotcha. When you replay a captured request, you have a choice: send it with the original signature the provider attached, or re-sign it yourself with your signing secret.

If you keep the original signature, expect verification to fail in any well-built handler. Most providers sign a timestamp along with the body, and verification libraries enforce a timestamp tolerance — Stripe's default is five minutes. A request replayed an hour later carries a signature whose embedded timestamp is an hour old, so the handler rejects it. This is not a bug; it is the defense against a replay attack, where an attacker who intercepts a signed webhook re-sends it to trigger your handler again. Your replay tooling and an attacker's replay look identical to the handler, and the timestamp check is what stops both.

So how do you replay through a verifying handler? Three options, in order of preference:

  1. Re-sign the payload. Compute a fresh signature with a current timestamp using your test or staging signing secret. The handler verifies it like a normal delivery — no code changes needed.
  2. Disable verification behind an environment flag. In development, skip the signature check when an env var like SKIP_WEBHOOK_VERIFY is set. Never let this flag work in production.
  3. Widen the tolerance in dev only. Some libraries accept a tolerance parameter; setting it high locally lets original signatures pass. Riskier, because it is easy to forget.

Webhook testing tools handle this for you — HookSense replays let you adjust headers before sending, so you can strip or swap the signature header to match whichever strategy your handler expects.

Replaying to Localhost: The Workflow

The highest-leverage replay target is your own machine, because that is where the debugger lives. The workflow with HookSense looks like this:

  1. Point the provider at a capture endpoint. Create a HookSense endpoint and register its URL in your provider's webhook settings. Every delivery is captured in full — headers, body, query string.
  2. Stream to localhost with the CLI. Run the listener against the port your dev server uses:
npx hooksense listen -p 3000

Captured requests are forwarded to localhost:3000, so live webhooks hit your local handler in real time while you develop.

  1. Replay on demand. When a request fails — or when you want to re-test after a fix — hit replay on the captured request. Edit the body or headers first if you are probing an edge case. Set breakpoints, replay, step through, fix, replay again.
  2. Promote to staging. Once the handler passes locally, replay the same captured request at your staging URL to verify the fix in a deployed environment.

One safety note: replay targets are validated against SSRF, so requests to internal or private IP ranges are blocked — you cannot accidentally (or maliciously) aim a replay at infrastructure that should not be reachable.

On limits: the free Catch plan includes 5 replays per month with 14-day request retention, which is enough to evaluate the workflow. The Hook plan ($19/mo) and Sense plan ($49/mo) include unlimited replays with 30-day and 90-day retention respectively — retention matters for incident recovery, since you can only replay what is still stored.

The Webhook Replay Checklist

Before you replay, run down this list:

  • Is the handler idempotent? Deduplicate by event ID before replaying against any environment with real side effects.
  • What is the target? Prefer localhost or staging for debugging; reserve production replays for backfill after verified fixes.
  • How will the signature be handled? Re-sign with a test secret, or disable verification behind a dev-only flag. Expect original signatures to fail timestamp checks.
  • Does the payload need edits? Adjust method, headers, or body before sending if you are testing a variant rather than reproducing the original.
  • Is the event still retained? Check your retention window — you cannot replay a request that has aged out of storage.
  • Did you verify the response? A replay that returns 200 is only meaningful if you confirm the downstream effect (database row, state transition) actually happened — once.

Wrapping Up

Webhook replay turns the worst property of webhooks — that they are ephemeral, one-shot deliveries you cannot reproduce — into a non-issue. Capture everything, and any request becomes repeatable: re-run the payload that broke your handler, mutate a real event into an edge-case test, debug incidents on localhost, and backfill outages. Just remember the two rules: idempotent handlers make replays safe, and timestamp-checked signatures make stale replays fail by design.

HookSense captures every request to your endpoint and replays any of them to any URL, with edits, from the dashboard or CLI. Read the replay documentation to get started, or create a free endpoint and replay your first captured webhook in under a minute.

Related posts

Try HookSense Free

Inspect, debug, and replay webhooks in real-time. No credit card required.

Get Started Free