CanHook
Features Pricing Docs FAQ Blog Log in Get started free

How to Replay Captured Webhooks to Your Local Dev Server

By The CanHook Team · August 27, 2026 · 1060 words
In short

Point the provider at a hosted catch URL to capture one real webhook, then use the Replay button to resend that exact payload to http://localhost:PORT as many times as you need, no tunnel required.

Testing a webhook against code running on your laptop usually means standing up a tunnel just so a provider like Stripe or GitHub has a public URL to hit. There's a faster path for most of a debugging session: capture the webhook once with a hosted catch URL, then replay the exact payload to localhost as many times as you need. No tunnel process to keep alive, no subdomain to re-paste into a provider's dashboard every time it restarts.

Why a tunnel is overkill for most local testing

A tunnel solves one specific problem: giving a remote sender a public URL that forwards to a process on a machine that doesn't have one. That's necessary the first time a provider needs to reach you. It's not necessary for the next thirty times you re-trigger the same event while you fix a null check in your handler. Re-firing a real Stripe test charge or pushing an empty commit just to get GitHub to resend a webhook is slow, and some events are awkward to trigger twice on demand. Capture the real payload once, then fire it at localhost repeatedly without needing the tunnel up or the provider's cooperation at all. (If you haven't captured a webhook before, the getting started guide covers creating your first endpoint; this post picks up from there.)

Step 1 — point the provider at a catch URL

Create an endpoint in the CanHook dashboard and you get a catch URL in the form https://canhook.com/h/{token}. It accepts any HTTP method and stores the method, headers, body and content type, and returns the endpoint's configured mock response to the sender. Swap it into the provider's webhook settings temporarily — Stripe Dashboard > Developers > Webhooks, or a GitHub repo's Settings > Webhooks — and trigger the real action once: a real test-mode charge, a real push. If you just want to try the flow without wiring up a real provider, you can post a fake event straight at the catch URL yourself:

curl -X POST https://canhook.com/h/abc123def456 \
  -H "Content-Type: application/json" \
  -d '{"id":"evt_test_1","type":"payment_intent.succeeded"}'

Step 2 — inspect what actually arrived

The endpoint's request list shows every capture as it comes in. Expand a row and you get the raw headers, the content type, and the body pretty-printed if it parses as JSON, with a one-click copy of the raw body. This is also where you catch the gap between what you assumed a provider sends and what it actually sends — a form-encoded body where you expected JSON, a header casing you didn't account for, an event type you hadn't handled. It's a cheap way to settle an argument with yourself about whether a bug is in your handler or in an assumption about the payload — the capture either matches your mental model or it doesn't, and you find out before you've spent an hour stepping through a debugger.

Step 3 — replay it straight to your dev server

Next to each captured request is a Replay field and button. Paste in http://localhost:3000/webhooks/stripe (or wherever your handler listens) and click Replay. This matters because of how CanHook's two forwarding paths differ: relay rules run entirely on CanHook's own servers and are deliberately blocked by an SSRF guard from ever targeting a private or loopback address, so a relay rule can never point at your laptop. The Replay button works differently — it's a fetch call made by your own browser tab, on your own network, so it can reach a localhost or 127.0.0.1 address that CanHook's server-side relay engine is built to refuse.

When the browser blocks it

Browser fetch is still subject to CORS, not SSRF, so a dev server that doesn't send an Access-Control-Allow-Origin header for the canhook.com origin will refuse the replay request. Either add a temporary CORS header to your local server for the duration of the debugging session:

app.use((req, res, next) => {\n  res.header('Access-Control-Allow-Origin', 'https://canhook.com');\n  res.header('Access-Control-Allow-Methods', 'POST');\n  res.header('Access-Control-Allow-Headers', 'Content-Type');\n  next();\n});

…or skip the button and use the Copy control on the request detail panel to grab the raw body, then send it yourself with curl, which isn't a browser and has no CORS policy to satisfy:

curl -X POST http://localhost:3000/webhooks/stripe \
  -H "Content-Type: application/json" \
  -d '{"id":"evt_test_1","type":"payment_intent.succeeded"}'

What replay does and doesn't preserve

The Replay button resends the exact body bytes with the original content type, but it always sends as a POST regardless of what method the original request used, and it does not resend the original request's other headers. That means a provider's signature header — Stripe-Signature, X-Hub-Signature-256 — is not part of the replay. If your handler verifies a signature before it does anything else, a replayed request will fail that check, which is correct behavior for the handler and not a bug in the replay. Test the signature-verification path itself with a real send from the provider first (see the guide to verifying GitHub's HMAC signature if that's the provider you're working with); use replay for iterating on everything downstream of it. For the broader mechanics of capturing and inspecting requests, the webhook testing and debugging guide is the starting point this post builds on.

Turning captures into fixtures

A captured body is a real payload with the real field set, real nesting, and real edge cases a hand-typed fixture usually misses — optional fields a provider only sends sometimes, numbers that arrive as strings, nested objects that are null instead of absent. Copy it into a committed test file the first time you capture it, before retention prunes it away — the free plan keeps captures for 24 hours, which is plenty of time to grab what you need but not a place to leave your only copy; the paid tiers stretch that to 7 or 30 days, see plans and retention if you need longer. From then on your test suite exercises the actual shape a provider sends instead of a guess at it.

None of this needs a paid plan or a relay rule configured; the catch URL and the Replay button are both available on the free tier, so the whole loop — capture, inspect, replay, fix, replay again — costs nothing but the endpoint you already created.

Frequently asked questions

Can I replay a captured webhook straight to localhost?

Yes. The Replay button on a captured request runs as a fetch from your own browser tab rather than from CanHook's servers, so it can reach http://localhost:3000 or 127.0.0.1 directly, which the server-side relay engine cannot.

Why can't a relay rule forward to localhost, but Replay can?

Relay rules execute on CanHook's own servers and are blocked from targeting private or loopback addresses by an SSRF guard. The Replay button is client-side JavaScript running in your browser on your own network, so that restriction doesn't apply to it.

Why does my local server return a CORS error when I click Replay?

Replay is a browser fetch call, so it's subject to CORS. Add an Access-Control-Allow-Origin header for https://canhook.com to your dev server, or copy the raw body and send it with curl instead, which has no CORS policy.

Does replaying a webhook preserve the provider's signature header?

No. Replay resends the captured body and content type but not the original request's other headers, including signature headers like Stripe-Signature or X-Hub-Signature-256. Test signature verification with a real send first, then use replay for everything downstream.

Does the replayed request use the original HTTP method?

No, the Replay button always sends the replayed request as a POST regardless of which method the original webhook used. Almost all provider webhooks are POST already, so this rarely matters in practice.

Do I need a paid CanHook plan to capture and replay webhooks locally?

No. Creating a catch URL and using the Replay button are both available on the free plan, which allows 2 endpoints, 100 stored requests per endpoint, and 24 hours of retention.

How long do captured webhook payloads stay available to replay?

Retention depends on plan: 24 hours on the free plan, 7 days on Pro, and 30 days on Business. Copy any payload you want to keep as a test fixture before it's pruned.