Your app runs on http://localhost:3000, but GitHub/Stripe/Slack can only
deliver webhooks to a public URL. The usual answer is a tunnel (ngrok, cloudflared,
localtunnel) — which means installing a daemon, keeping a session alive, and on many
corporate networks it's blocked outright. There's a simpler way: capture the webhook at a
public bin, and relay it to localhost by polling from your side.
$ curl https://catchhook.catchhook.workers.dev/new
Paste the /h/… capture URL into the provider's webhook settings
(GitHub repo → Settings → Webhooks, Stripe → Developers → Webhooks, …).
$ curl -s https://catchhook.catchhook.workers.dev/cli -o catchhook && chmod +x catchhook $ ./catchhook relay YOUR_BIN http://localhost:3000
That's it. Every webhook that hits your bin is re-delivered to your local server within a couple of seconds, with:
POST /h/YOUR_BIN/hooks/gh?x=1 arrives as POST http://localhost:3000/hooks/gh?x=1;Content-Type and signature headers — intact);X-Hub-Signature-256 or Stripe's Stripe-Signature still verify in your handler code.By default only new captures are relayed; add --all to replay the
bin's history too (handy to re-run a batch of deliveries against a fresh build).
curl https://catchhook.catchhook.workers.dev/cli.--all and nothing is lost.The trade-off: delivery adds a second or two of latency, and the webhook sender sees the
bin's response (customisable in settings), not your local server's. For inspecting and
developing handlers that's usually exactly what you want — the provider always gets a clean
200 while your handler is allowed to crash.
The relay is plain HTTP; you can build your own in any language:
# list new captures (tab-separated: id, method, path?query) curl 'https://catchhook.catchhook.workers.dev/api/bins/YOUR_BIN/relay-list?after=0' # fetch one capture's raw body and forwardable headers curl https://catchhook.catchhook.workers.dev/api/bins/YOUR_BIN/req/REQUEST_ID/body curl https://catchhook.catchhook.workers.dev/api/bins/YOUR_BIN/req/REQUEST_ID/fwd-headers
The x-catchhook-last response header on relay-list is the
cursor for your next poll; ?peek=1 returns just the current cursor so you can
start "from now".
There is a longer walk-through with real transcripts (GitHub-style HMAC verification end-to-end through the relay) on dev.to: Webhooks to localhost without ngrok (or any tunnel).
No signup needed. Or from your terminal: curl https://catchhook.catchhook.workers.dev/new