How to Replay Webhooks for Faster Debugging
When you are building a webhook handler, you run the same cycle over and over: trigger an event, check your handler, find the bug, fix it, trigger again. The problem is that "trigger again" often means going back to Stripe's dashboard, clicking through a checkout flow, or pushing a commit to GitHub just to fire another push event.
This is slow. It breaks your focus. And some events are hard to reproduce on demand — a failed payment, a subscription that just expired, a specific edge-case payload.
Webhook replay solves this. You capture the event once, then replay it as many times as you need against your local server while you iterate on your handler.
What Webhook Replay Actually Does
Replay takes a previously captured webhook delivery and re-sends it to a URL you specify. The re-send includes the original HTTP body and — depending on the tool — the original headers.
This is different from "resending" in a webhook provider's dashboard (like Stripe's "Resend" button). That triggers a new delivery attempt from Stripe's infrastructure, which:
- Creates a new delivery record with a new timestamp
- Re-runs Stripe's retry logic
- Requires network access to your endpoint (your local server needs to be publicly reachable)
Replay from your testing tool is different: it sends the stored payload directly from your browser or the testing service to whatever URL you type in. No round-trip to Stripe. No public URL required. You can target http://localhost:3000/webhooks/stripe directly.
The Development Loop With and Without Replay
Without replay, the typical debug cycle looks like this:
- Trigger a real event (complete a test checkout, push a commit, submit a form)
- Watch the webhook arrive in your testing tool
- Notice your handler returned a 500 or processed the payload incorrectly
- Fix the bug in your code
- Go back to the event source and trigger the same event again
- Wait for delivery
The round-trip to the event source is the bottleneck. Some events are one-click to trigger, but others require specific conditions: a card that declines on the third retry, a subscription that just crossed its trial end date, a GitHub PR that was merged after a specific label was applied. You can not easily manufacture those conditions on demand.
With replay, steps 5 and 6 become: click Replay, enter http://localhost:3000/webhooks/stripe, click Send.
The exact same payload your handler failed on goes to your fixed handler. No event source required.
Setting Up Webhook Replay with HookCap
Step 1: Capture a real delivery
Create an endpoint in HookCap at hookcap.dev. You get a permanent URL like:
https://hook.hookcap.dev/ep_a1b2c3d4e5f6
Configure this URL in your webhook source (Stripe, GitHub, Shopify, Slack, or anything else that sends webhooks). Trigger the event once to capture a real delivery.
HookCap stores the full request: method, headers, body, timestamp, and response status from your endpoint.
Step 2: Inspect the captured payload
Click on the delivery in your dashboard. You will see the raw request body, all headers, and the delivery timeline. This is useful before you replay — check that the payload structure matches what your handler expects.
For Stripe specifically, you will see headers like:
Stripe-Signature: t=1711900800,v1=abc123...
Content-Type: application/json
Stripe-Idempotency-Key: evt_1234...
Step 3: Replay against your local server
In the delivery detail view, click Replay. Enter your local handler URL:
http://localhost:3000/webhooks/stripe
HookCap sends the captured payload to your local server and shows the response. You see the status code and response body immediately.
Step 4: Iterate
Fix your handler, click Replay again. Repeat until the response is 200 {"received": true} (or whatever your handler returns on success).
The payload does not change between replays. You are testing the same bytes your handler failed on originally.
Handling Signature Verification During Replay
Most webhook providers sign their payloads. Stripe uses HMAC-SHA256 with a signing secret per endpoint. When you replay to localhost, your handler will try to verify the Stripe-Signature header — but the signature was computed for the original HookCap endpoint URL, not your local server.
There are two practical approaches:
Option A: Disable signature verification in development
Add a flag to skip verification when running locally:
if (process.env.NODE_ENV !== 'development') {
const sig = req.headers['stripe-signature'];
event = stripe.webhooks.constructEvent(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET);
} else {
event = JSON.parse(rawBody);
}
This is the fastest option during active development. Make sure the NODE_ENV check is solid before you deploy.
Option B: Use a test signing secret
In Stripe, your test mode webhook endpoint has its own signing secret (whsec_test_...). When replaying to your local server, you can use the Stripe CLI to forward events and apply your local signing secret — or use HookCap's re-sign feature to attach a valid signature for your local secret before replay.
Either approach means your handler runs through the full verification path, giving you higher confidence that it will work in production.
What to Replay and When
Replay is most valuable in three scenarios:
1. Debugging a broken handler
Your handler failed. You have the exact payload that caused the failure. Fix the code, replay, verify. This is the core use case.
2. Testing edge cases
Some events are hard to trigger on demand:
-
invoice.payment_failed— requires a card that declines on retry, not just on the first attempt -
customer.subscription.deleted— requires a subscription to be cancelled, which may involve billing cycles - GitHub's
pull_requestclosed event after a merge — requires an actual merge
Capture these once when they occur naturally, then replay them anytime.
3. Regression testing
When you change your webhook handler, replay a set of known-good deliveries to verify nothing broke. This is not a substitute for unit tests, but it is a fast sanity check before you ship.
Replay vs. Resend: When to Use Each
| Replay (testing tool) | Resend (provider dashboard) | |
|---|---|---|
| Destination | Any URL, including localhost | Must be a registered, publicly reachable endpoint |
| Network access | Not required for source | Required |
| Signature | Original or re-signed | New signature from provider |
| Speed | Instant | Depends on provider retry queue |
| Use case | Active development | Reproducing a missed delivery in production |
Use replay when you are developing. Use the provider's resend when you need to reprocess a specific delivery that failed in production.
A Note on Idempotency
If you replay the same event multiple times, your handler will receive the same event ID each time. Your handler should be idempotent — processing the same event twice should not create duplicate records or charge a customer twice.
Test this deliberately during development:
- Replay an event that creates a database record
- Replay it again
- Verify you have one record, not two
The pattern is simple:
// Check if we already processed this event
const existing = await db.query(
'SELECT id FROM processed_events WHERE event_id = $1',
[event.id]
);
if (existing.rows.length > 0) {
return res.status(200).json({ received: true }); // Already handled
}
// Process the event, then mark it as handled
await processEvent(event);
await db.query(
'INSERT INTO processed_events (event_id, processed_at) VALUES ($1, NOW())',
[event.id]
);
Replay makes idempotency testing easy because you can hit the same endpoint with the same event ID as many times as you want.
Why This Matters More Than You Think
The debugging loop improvement is real but understated. Replay does not just save the time of re-triggering events — it changes what is possible to test.
Hard-to-reproduce events stop being hard to test once you have captured them. Edge cases that only appear under specific business conditions become repeatable. And because you are testing against your actual handler with the actual payload that caused the failure, there are no surprises when you deploy.
This is the difference between a capture tool and a debugging tool. Capture shows you what arrived. Replay lets you work with what you captured.
Start capturing and replaying webhooks for free at hookcap.dev. Replay is available on all plans.
Top comments (0)