Skip to content
catchook

Guides

Local development

Receive real provider webhooks on your laptop, with no tunnel and no public URL.

Webhook handlers are awkward to develop because the provider needs a public URL and your machine does not have one. The usual answer is a tunnel, which gives the internet a door into your laptop and changes address every restart.

Catchook already receives the webhook. catchook listen opens an outbound connection and forwards each one to a local port:

npx @catchook/cli login
npx @catchook/cli listen --forward-to http://localhost:3000/webhooks

Nothing listens on your machine from outside. The connection is outbound, so it works behind NAT, on a VPN, and on hotel wifi.

What your local server gets

The same request the provider sent — the body byte for byte, the original headers including the provider’s signature, plus Catchook’s catchook-* headers. Your real verification code runs against a real signed request, rather than against a hand-written fixture that may not match.

Replaying while you iterate

The interesting part of webhook development is the second attempt. listen keeps every payload it forwarded, so replaying is instant and needs no network call:

↑↓ select · enter replay · a all · space pause · tab detail · ? help · q quit

Every forward is kept, not just the latest — each with its status, its latency, and what your server answered. A 500 with a stack trace in the body is the point.

To replay something from before this session, replay reads from the API instead:

catchook replay                    # the most recent event
catchook replay --last 20          # the last 20, oldest first
catchook replay evt_d70ac863...    # one specific event

Two replays, never implicit

  • catchook replay --forward-to <url> goes to your machine and is recorded nowhere.
  • catchook replay --to-destinations goes through real delivery: the queue, the retries, and a row in the delivery history.

With neither, the command refuses rather than guessing. That is deliberate: accidentally re-sending production events to real destinations while debugging is not a recoverable mistake.

Signing a local replay

If your handler verifies Catchook’s own catchook-signature, a plain local replay will fail that check — nothing signed it. Pass the destination’s secret:

catchook listen --forward-to http://localhost:3000 --sign-with whsec_...

This signs through the same code path production deliveries use. A local replay that signed differently would “prove” your receiver works when production would reject every delivery.

Pointing at a local Catchook

Working on Catchook itself rather than against it:

CATCHOOK_API_URL=http://localhost:8787 \
CATCHOOK_EVENTS_URL=http://localhost:8788 \
CATCHOOK_API_KEY=ck_... \
catchook listen --forward-to http://localhost:3000