Launch special — let's split the check with SPLITCHECK for 50% off
9 min read

How to Receive Webhooks on Localhost: Tunnels, Provider CLIs, and Pull-Forwarding

Three ways to forward webhooks to localhost — tunnels, provider CLIs, and pull-forwarding — plus a full local development walkthrough with replay.

webhookslocal developmentclitutorial
O

Ozer

Developer & Founder of HookSense

You cannot receive webhooks on localhost directly — Stripe, GitHub, Shopify, and every other provider need a public HTTPS URL, and "http://localhost:3000" is only reachable from your own machine. The fix is a bridge between the public internet and your local server, and there are three ways to build one: a tunnel (ngrok or cloudflared), a provider-specific CLI (like stripe listen), or an inspector with pull-forwarding (the HookSense CLI). This guide covers all three honestly, then walks through a complete local development workflow: capture an event, hit your local handler, fix the bug, and replay the same event without re-triggering it.

Why Localhost Can't Receive Webhooks

A webhook is just an HTTP POST from the provider's servers to a URL you registered. The provider's infrastructure sits somewhere on the public internet; your dev machine sits behind a router doing NAT, probably behind a corporate firewall, with no public IP and no inbound ports open. When Stripe tries to POST to your handler, there is simply no route to your laptop.

Every solution to this problem does one of two things: it either pushes traffic into your machine by opening a route from the internet (tunnels), or it pulls events down from a cloud capture point using an outbound connection your firewall already allows (provider CLIs and pull-forwarding inspectors).

Approach 1: Tunnels (ngrok, cloudflared)

Tunneling tools create a public URL that forwards traffic into your local port. You run a command, get a URL like "https://a1b2c3.ngrok-free.app", and register it with your provider.

ngrok http 3000
# or
cloudflared tunnel --url http://localhost:3000

Pros:

  • Works for any HTTP traffic, not just webhooks — you can demo a whole app.
  • Mature, well-documented tools with traffic inspection built in.
  • Requests reach your server in true real time with no polling.

Cons:

  • The URL changes every time you restart the tunnel on free tiers, so you re-register it with every provider, every session. Static domains cost money.
  • Nothing is captured when the tunnel is down. Close your laptop and the webhook is gone — you find out later when state has drifted.
  • You are exposing your machine to the public internet, which many corporate security policies prohibit outright. Some networks block tunnel traffic entirely.
  • Authtokens, account setup, and rate limits add friction for a quick debugging session.

If you mainly want a tunnel for webhooks specifically, we wrote a deeper comparison in our ngrok alternative guide.

Approach 2: Provider CLIs (stripe listen)

Some providers ship their own forwarding tools. Stripe's is the best known:

stripe listen --forward-to localhost:3000/webhooks/stripe

This is genuinely great when it exists. The CLI authenticates with your Stripe account, streams events as they happen, and even prints a signing secret so you can test signature verification properly. If you only ever work with Stripe, use it — we have a full guide to testing Stripe webhooks locally.

The limitation is in the name: it is provider-specific. There is no "github listen", no "shopify listen", no equivalent for the SaaS tool your customer integrates with. The moment your app receives webhooks from two or more sources — and most real apps do — you are juggling one workflow for Stripe and something entirely different for everyone else. Events also are not stored anywhere you can browse later: once the CLI session ends, the history is gone.

Approach 3: Inspector with Pull-Forwarding (HookSense CLI)

The third approach splits the problem in two. A cloud endpoint with a permanent URL captures every webhook, from any provider, whether your machine is on or not. Then a CLI on your machine pulls those events down over HTTPS and POSTs them to your local server.

npx hooksense listen -p 3000

The direction of that connection is the key detail. The CLI only makes outbound HTTPS requests — the same kind your browser makes. That means:

  • No tunnel and no exposed machine. Nothing on the public internet can reach your laptop.
  • No inbound ports, no authtoken, no firewall changes. It works behind corporate firewalls, NAT, hotel Wi-Fi, and VPNs where tunnels are blocked or banned.
  • The URL never changes. Register it with your providers once. Restart your laptop, switch networks, come back next week — same URL.
  • Nothing is ever missed. Events fired while your dev server was off are captured in the cloud, waiting. You can forward or replay them whenever you are ready.

The trade-off is honest too: this approach is built for webhooks, not for exposing an arbitrary web app to the internet. If you need to share a running frontend with a client, a tunnel is still the right tool. For webhook development specifically, pull-forwarding removes the failure modes that make tunnels painful.

The CLI is open source at github.com/ozers/hooksense-cli, and the full flag reference lives in the CLI docs.

Full Walkthrough: From Zero to Replay

Here is the complete workflow, end to end. It takes about two minutes the first time.

Step 1: Create an endpoint (no signup)

Go to hooksense.com and create an endpoint — you get a unique URL in about a second, no account required. Anonymous endpoints expire after 14 days if unclaimed; create a free account and claim it, and the endpoint is permanent. You never re-register URLs with providers again. (Details in Getting Started.)

Step 2: Register the URL with your provider

Paste your HookSense URL into the provider's webhook settings — the Stripe Dashboard under Developers, GitHub repo settings under Webhooks, wherever your provider configures deliveries. Because the URL is permanent, this is a one-time step per provider.

Step 3: Start the CLI

npx hooksense listen -p 3000

That forwards every captured event to "http://localhost:3000". If your handler lives on a specific route, map it with the path flag:

npx hooksense listen -p 3000 --path /webhooks/stripe

No install step, no authtoken, no config file. The CLI connects out over HTTPS and starts delivering.

Step 4: Trigger an event

Push a commit, fire a test payment, click "send test webhook" in the provider dashboard — whatever produces an event. Two things happen at once: the event appears instantly in the HookSense UI over a real-time WebSocket connection (headers, raw body, query params, all of it), and the CLI POSTs the same request to your local server.

Step 5: Fix the bug and replay

This is where the workflow pays off. Say your handler returned a 500 because it expected "data.object.id" and the payload nested it differently. With a tunnel, your next step is re-triggering the event — another test payment, another commit, another walk through the provider dashboard. With a captured event, you just fix the code and hit Replay. The exact same payload, byte for byte, hits your local server again. Still broken? Replay again. You can iterate on one real event a dozen times in the time it takes to re-trigger it once.

Replays also support edits: tweak the body or headers before sending to probe edge cases — a missing field, a malformed timestamp, an unexpected event type — without waiting for the provider to produce them naturally.

Testing Signature Verification Locally

Most providers sign webhooks with an HMAC signature computed over the raw request body, and signature verification is the single most common thing that works in production but fails on localhost. The usual culprit: your framework parses the JSON body before your verification code sees it, so you are computing the HMAC over re-serialized bytes that no longer match what was signed.

Because HookSense captures the exact raw body and all original headers — and the CLI forwards them unchanged — your local handler verifies against the same bytes the provider signed. A typical Express setup:

const crypto = require("crypto");

app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-signature"];
  const expected = crypto
    .createHmac("sha256", process.env.WEBHOOK_SECRET)
    .update(req.body)
    .digest("hex");

  const valid = crypto.timingSafeEqual(
    Buffer.from(signature, "hex"),
    Buffer.from(expected, "hex")
  );

  if (!valid) return res.status(401).send("invalid signature");
  res.status(200).send("ok");
});

If verification fails locally, open the captured request in the HookSense UI and compare the raw body there with what your handler received. If they differ, something in your middleware stack is rewriting the body before verification runs. Replay the same event after each fix until the check passes — no re-triggering required.

Team Workflows: Sharing a Captured Payload

Webhook bugs are rarely solved alone. "Can you send me the payload?" usually means a JSON blob pasted into Slack, stripped of headers, mangled by formatting, and missing the one field that mattered.

Instead, share the endpoint itself. HookSense endpoints support read-only share links: send a teammate the link and they see the same captured requests — full headers, raw bodies, timestamps — without being able to modify or delete anything, and without needing an account on your team. Your backend dev in another timezone can inspect the exact failing request from this morning, in context, next to every other delivery around it. See endpoint sharing docs for setup.

Which Approach Should You Use?

  • Use a tunnel when you need to expose a whole app — demos, frontend previews, OAuth callbacks during early prototyping.
  • Use the provider CLI when one exists and you only work with that provider — stripe listen is excellent for Stripe-only work.
  • Use an inspector with pull-forwarding when webhooks are the job: multiple providers, signature debugging, replay-driven iteration, corporate networks where tunnels are blocked, or teams that need to look at the same payloads.

The free Catch plan covers 3 endpoints, 300 requests a day, and 14 days of retention — enough for most local development. The Hook plan ($19/mo) raises that to 15 endpoints, 5,000 requests a day, 30-day retention, HMAC verification tooling, and unlimited replays; Sense ($49/mo) removes the ceilings for heavier teams.

Grab a URL at hooksense.com, run "npx hooksense listen -p 3000", and your localhost is receiving webhooks — no tunnel, no open ports, no re-registering URLs ever again.

Related posts

Try HookSense Free

Inspect, debug, and replay webhooks in real-time. No credit card required.

Get Started Free