How to test webhooks without ngrok

Three ways to test webhooks without ngrok: a no-signup quick tunnel (cloudflared, localhost.run over SSH, localtunnel), a provider CLI that forwards events to localhost (stripe listen, gh webhook forward), or a free request bin when you only need the payload.

ngrok works, but the friction adds up: an account before your first tunnel, an authtoken to paste, a URL that changes every restart unless you pay, and a desktop app that phones home. If all you want is "make Stripe hit my laptop", there are lighter options — some need no signup, some need no install, and some skip the tunnel entirely. This guide compares the ones that actually work in 2026. For a head-to-head on the inspection side, see webhooks.sh vs ngrok; for the broader local-testing loop, see testing webhooks locally.

First: which problem are you solving?

"Test webhooks locally" is two different jobs wearing the same coat:

  • Tunnel — get internet traffic to reach your local dev server, so your real handler code runs on real deliveries.
  • Mailbox — give the provider a public URL that records requests, so you can inspect exactly what was sent: headers, raw body, signature, timing.

ngrok does the first (with a local inspector bolted on). Many setups only need the second — especially early on, when you don't even know the payload shape yet. Pick the tool for the job you're actually doing.

Quick tunnels, no signup

cloudflared quick tunnel — the best free tunnel right now

Cloudflare's cloudflared binary can spin up an unauthenticated "quick tunnel" (TryCloudflare). One binary to install, zero account, and it rides Cloudflare's network — which makes it fast and reliable:

cloudflared tunnel --url http://localhost:3000

It prints a random https://….trycloudflare.com URL that proxies to your local server for as long as the process runs. New URL every restart, and it stores nothing — you see traffic in your own app's logs, not in a dashboard. Install via Homebrew (brew install cloudflared), a package from Cloudflare's repos, or Docker.

localhost.run — nothing to install at all

localhost.run tunnels over SSH, which you already have. No download, no signup:

ssh -R 80:localhost:3000 localhost.run

The session prints a random public URL that forwards to your port 3000 until you close it. Free domains are on lhr.rocks subdomains and change every session; stable custom domains are a paid extra. Because it's an SSH session, add keepalives (-o ServerAliveInterval=60) if your network drops idle connections. Ideal on locked-down machines where you can't install binaries.

localtunnel — the npm classic, with caveats

npx localtunnel --port 3000

Still works, needs only Node, and lets you request a memorable subdomain (--subdomain myapp) when it's free. Honesty corner: localtunnel has had flaky stretches over the years, and it now shows first-time visitors an anti-abuse interstitial asking for a "tunnel password" — which is your public IP. Webhook providers don't click through interstitials, so if deliveries seem to vanish, check whether the provider is getting that page instead of your app. When it behaves, it's the fastest npm-native option; when it doesn't, switch to cloudflared.

zrok — free but signup required

zrok (built on OpenZiti) has a generous free tier with reserved shares — a stable URL that survives restarts, which none of the no-signup options give you. The catch: you need a free account and a one-time zrok enable before sharing:

zrok share public http://localhost:3000

Worth it when you paste the URL into a provider dashboard once and don't want to update it every morning.

Skip the tunnel entirely: provider CLIs

For the two biggest senders, you don't need a public URL at all — their CLIs pull events down to you over a websocket:

stripe listen --forward-to localhost:3000/webhooks/stripe
stripe trigger payment_intent.succeeded

stripe listen authenticates with your Stripe account, prints its own whsec_… signing secret for the session (different from your Dashboard endpoint secret — use the right one), and forwards every event to your local handler. stripe trigger fabricates events on demand, which beats clicking around the Dashboard to manufacture a failed payment.

GitHub's CLI has the same trick built in:

gh webhook forward --repo you/yourrepo --events push --url http://localhost:3000/webhooks/github

Real deliveries from that repo get forwarded to your machine, no public endpoint needed. Limitations: these only cover their own providers, and they don't give you a URL to hand to other senders.

The mailbox approach: a free bin

When the question is "what does this provider actually send", skip tunnels entirely. Create a bin at view.webhooks.page — no signup, no install — and paste its URL into the provider's webhook settings. Every delivery lands in your browser live: method, full headers, raw body, timing. That's the ground truth for payload shapes, signature headers, and retry behavior.

Bins expire after 48 hours and don't forward to localhost. Solo ($12/month) does: wsh listen relays deliveries from a persistent endpoint to localhost, no tunnel needed. On a free bin, capture a real delivery, then replay it at your local handler yourself:

curl -X POST http://localhost:3000/webhooks/stripe \
  -H "Content-Type: application/json" \
  -H "Stripe-Signature: t=…,v1=…" \
  --data-binary @captured-body.json

Copy the raw body byte-for-byte — re-serialized JSON breaks signature verification, as the signature verifier will happily demonstrate.

Comparison

ToolSignup?Install?Persistent URLStores requests
ngrok (free tier)YesYesPaid onlyYes — local inspector on :4040
cloudflared quick tunnelNoOne binaryNo — random per runNo
localhost.runNoNo — uses sshNo — custom domains paidNo
localtunnelNonpx (Node)Sort of — --subdomain when freeNo
zrokYes — freeYesYes — reserved sharesNo
stripe listen / gh webhook forwardProvider accountYesn/a — no public URLPrints to terminal
webhooks.sh binNoNoWith Solo, $12/moYes — 48h in browser

So which one, when?

  • "I just want to see what the provider sends." A bin. Zero setup, and you get the raw request instead of your framework's interpretation of it.
  • "I'm building a Stripe or GitHub integration." The provider CLI (stripe listen, gh webhook forward). Real events at your real handler, no URL juggling.
  • "Any other provider, driving local code, no signup." cloudflared quick tunnel — the most reliable free tunnel today.
  • "I can't install anything." localhost.run over the ssh client you already have.
  • "I need the same URL every day." zrok reserved shares for free, or a persistent webhooks.sh Solo endpoint for $12/month.

The honest summary: ngrok's moat was being the only polished option. In 2026 the free tier of your workflow is cloudflared or a provider CLI for tunnels, plus a bin for inspection — and you only reach for ngrok when you specifically want its inspector or a reserved domain on its free-ish terms.

Last updated

Try it in 10 seconds.

Create a free bin at view.webhooks.page — no signup, no install. Point your sender at the URL and watch requests land live. Bins expire after 48 hours.