Core

Receive Real Webhooks on localhost — The URL Never Changes

It's 3pm. You're building the Stripe subscription upgrade flow. You've been testing against hand-crafted JSON payloads for two hours. Your PM asks if it's ready for tomorrow's demo.

Then you realise: you've been testing against a payload structure from the Stripe documentation. You've never seen what a real customer.subscription.updated event looks like for a customer who's upgrading from Free to Pro after a failed payment. You don't know what fields are present. You don't know what previous_attributes contains. You're flying blind.

This is the local development webhook problem. Real events are on the internet. Your server is on localhost. The gap between them is where bugs live.

How developers usually solve this

ngrok: A real solution, but with real friction. Install the CLI, authenticate with an ngrok account, start the tunnel, copy the URL (which is different every session on the free plan), paste it into Stripe's webhook settings, test, tunnel restarts, reconfigure Stripe, repeat.

The deeper problem: every time your ngrok URL changes, your Stripe configuration is stale. You now have a webhook endpoint in Stripe pointing to a URL that no longer exists. Every developer on your team has their own tunnel URL. Each one has to be configured separately.

Deploy to staging for every test: Slower than slow. Your CI/CD pipeline becomes part of your development loop. You're testing in a shared environment where other people's changes can affect your results.

Manual payload construction: You write JSON that you hope matches what Stripe actually sends. It never fully matches. Edge cases always find you.

The HookTunnel approach: capture first, forward second

HookTunnel separates the concerns:

  1. Permanent URL in your Stripe dashboard — configured once, never changes
  2. Events captured at the HookTunnel level regardless of your local server state
  3. WebSocket tunnel forwards captured events to your localhost on demand

The permanent URL is the key. When you set https://hooks.hooktunnel.com/h/abc123 in Stripe, that URL never changes. Whether you're running the tunnel or not. Whether your local server is up or down.

What happens when your tunnel is offline

Events keep arriving at your HookTunnel URL. They're captured and stored. Your local server never sees them — and that's fine.

When you reconnect your tunnel, you can replay the captured events against your local server. The events Stripe sent while you were offline are waiting for you. No events lost.

This is a fundamental difference from ngrok: ngrok drops events when the tunnel is offline. HookTunnel captures them.

The developer experience

# Install the CLI
npm install -g @hooktunnel/cli

# Connect to your hook
hooktunnel forward --hook abc123xyz --to http://localhost:3000/webhooks/stripe

# Output:
# Connected
# Forwarding: hooks.hooktunnel.com/h/abc123xyz -> localhost:3000/webhooks/stripe
# 3 events captured while offline -- replay? [y/N]

When an event arrives, it's forwarded to your local server in real time. The response from your server is captured and shown in the HookTunnel dashboard.

The team development story

Your team of 4 developers all need to test webhook handlers locally. With ngrok: 4 tunnel URLs, 4 Stripe webhook configurations, constant "wait, which URL is yours?" confusion.

With HookTunnel: one shared hook URL in Stripe. Each developer runs the tunnel locally and receives events. Or one developer runs the tunnel and shares captured events with the team via replay. The Stripe configuration is set once and never touched.

Comparison with ngrok

| Feature | HookTunnel | ngrok Free | |---|---|---| | Permanent URL | Yes | No (new URL per session) | | Events captured when offline | Yes | No | | Replay missed events on reconnect | Yes | No | | Multiple team members, one URL | Yes | No | | Outcome verification (Applied proof) | Yes | No | | Works behind corporate firewall | Yes (outbound WSS only) | Yes |

Frequently asked questions

Does the URL change when I reconnect the tunnel? No. Your HookTunnel URL is permanent. Configure it in Stripe once. It works every time you reconnect.

What happens to events when my local server is offline? Captured and stored in your event history. Replay them when you reconnect.

How do I install the CLI? npm install -g @hooktunnel/cli. Authenticate with your API key from the HookTunnel dashboard (Settings → API Keys).

Does this work behind a corporate firewall? Yes. The tunnel uses outbound WebSocket (WSS) only — no inbound ports required. Works in most corporate network environments.

Can multiple developers receive the same events? One tunnel connection at a time per hook. Multiple developers can use replay to independently test against the same captured events.

How is this different from ngrok? Permanent URL (never changes), offline capture, replay on reconnect, and outcome verification. ngrok's inspect window is session-scoped and drops events when offline. HookTunnel stores everything.

Get a permanent local webhook tunnel

Persistent URL. Real provider events. No reconfiguration.

Get started free →