Webhook testing gets expensive when you pay for tools that do three things you could do with a tunnel and a log file. Here is the setup that costs nothing, what it genuinely cannot do, and the one category where paying is defensible.
Before writing any handler, point the provider at a throwaway inspection endpoint and look at a real payload. webhook.site gives you a unique URL with no signup and shows headers, body and timing. Pipedream's RequestBin does the same. Ten minutes here saves you from writing a parser against the documentation instead of against reality — the docs almost always omit a header or two, and the timestamp format is rarely what you assumed.
To get a real request into localhost:
cloudflared tunnel --url http://localhost:3000 — no account needed for a quick tunnel, gives you an HTTPS hostname immediately.ngrok http 3000 — free tier works, but the hostname changes on every restart, so you re-register the webhook each time.npx localtunnel --port 3000 — zero install, occasionally flaky under load.ssh -R 80:localhost:3000 serveo.net if you already have SSH and want no dependencies at all.Vendor CLIs are better still where they exist: stripe listen --forward-to localhost:3000/webhooks streams live events straight to your machine and signs them correctly, no tunnel involved.
The slowest part of webhook development is producing the event again — creating another test charge, another subscription cancellation. Capture one real payload to a file, then replay it as often as you like:
curl -X POST localhost:3000/webhooks -H "Content-Type: application/json" -H "X-Signature: ..." --data @event.json
Keep a directory of captured events per event type and commit it. That fixture set becomes your regression suite and costs nothing to maintain.
Most providers sign with HMAC-SHA256 over a timestamp plus the raw body. Two things break constantly: reading the body after a JSON middleware has already re-serialised it (the bytes change, the signature fails), and comparing signatures with == instead of a constant-time compare. Write one test that feeds a correctly signed fixture and one that feeds a tampered body, and assert the second returns 401. Also assert on timestamp tolerance, or you have built a replay-attack vector.
Return a 500 on purpose and watch what the provider does — most retry with exponential backoff for hours, which means your handler must be idempotent. Store the event ID and short-circuit duplicates. Then test slow responses: many providers time out at a handful of seconds, so the handler should enqueue work and return 200 immediately rather than processing inline.
Free tooling stops being enough once you need durable retries, a searchable event history across environments, and fan-out to several consumers. That is what managed gateways like Hookdeck or Svix exist for. Until you have production traffic and an on-call rotation, a tunnel, a fixture directory and an idempotency key cover the same ground for nothing.