How to Receive Webhooks on Localhost: 5 Ways Compared (2026)

Five ways to receive webhooks on localhost, with the exact command for each: Stripe CLI, ngrok, Cloudflare Tunnel, smee.io and Webhook Relay. Which one to use for a quick test, a team, or a server with no public IP.

How to receive webhooks on localhost

To receive webhooks on localhost you need a public URL that forwards to your machine, because providers such as Stripe, GitHub or Shopify can only POST to the public internet. There are five ways to get one: a provider's own CLI, a general-purpose tunnel (ngrok, Cloudflare Tunnel), a webhook proxy (smee.io), or a webhook relay with an agent (Webhook Relay). All of them work by opening an outbound connection from your laptop, so no ports are opened and no public IP is needed. They differ in whether the URL stays the same, what happens to events while you are offline, whether a team can share the endpoint, and how much you can inspect. Here is each one with its command, then a comparison and a recommendation.

1. Provider CLI (Stripe CLI, GitHub CLI): fastest for one provider

Some providers ship a CLI that subscribes to your account's events and forwards them locally.

stripe login
stripe listen --forward-to localhost:4242/webhook
stripe trigger payment_intent.succeeded   # fire a test event

The CLI prints a signing secret to use while it runs. GitHub has gh webhook forward for repository events.

Best for: developing against a single provider. Limits: one provider, one machine, no history, events are not stored while the CLI is off, and it does not help when the receiver is a server rather than your laptop. See Stripe CLI alternative for where it runs out.

2. ngrok: the classic tunnel

ngrok exposes a local port on a public hostname and shows every request in a local inspector.

ngrok http 3000

Give the provider the printed https://…ngrok… URL plus your path. Free accounts get one static development domain; more reserved domains, and higher limits, are paid.

Best for: exposing a whole local app for a demo or a quick webhook test. Limits: one tunnel per machine, nothing is stored when the tunnel is down, and free-plan session limits. Alternatives are compared in ngrok alternatives.

3. Cloudflare Tunnel: free tunnel, your own domain

cloudflared connects outbound to Cloudflare and publishes a local service.

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

That gives a temporary trycloudflare.com hostname with no account. With a domain on Cloudflare you can create a named tunnel and a stable hostname for free.

Best for: a free, stable public hostname for a local or self-hosted app. Limits: same as any tunnel for webhooks — nothing is stored while you are offline, and one hostname maps to one origin. See Cloudflare Tunnel alternative.

4. smee.io: webhook proxy for quick tests

smee.io, used in GitHub's own docs, gives you a channel URL and a client that replays events to localhost.

npx smee-client --url https://smee.io/YOUR-CHANNEL --target http://localhost:3000/events

Several clients can listen to one channel, so a small team can share it.

Best for: GitHub App development and quick experiments. Limits: no storage, no retries, no authentication, no request history; anyone with the channel URL can read your events. See smee.io alternative.

5. Webhook Relay: permanent URL, stored events, whole team

Webhook Relay gives you a bucket with a permanent public URL. The relay agent connects outbound and delivers each webhook to a local (or private-network) destination.

curl https://my.webhookrelay.com/webhookrelay/downloads/install-cli.sh | bash
relay login -k YOUR_TOKEN_KEY -s YOUR_TOKEN_SECRET
relay forward --bucket my-app http://localhost:3000/webhooks

Paste the bucket's public URL into the provider once. Every teammate who runs the same command against the same bucket receives every webhook. Requests are stored and shown in the logs; with durable retries or replay on connect, events that arrived while your laptop was closed are delivered when you are back. The same agent, Docker image or Kubernetes operator delivers to servers with no public IP, and functions can reshape or filter payloads before delivery.

Best for: webhook work that lasts longer than one session — a team, several providers on one endpoint, staging and laptop at once, or a private server. Limits: it is a webhook tool; for exposing an arbitrary app use its tunnels or one of the tunnels above. Free plan: 150 webhooks/month. Step-by-step: receive webhooks on localhost with the relay CLI.

Comparison

Provider CLIngrokCloudflare Tunnelsmee.ioWebhook Relay
Works for any providerNoYesYesYesYes
Public URL stays the samen/aOne static domain freeYes with your domainYes (channel)Yes, permanent
Events kept while you're offlineNoNoNoNoYes, retried up to 30 days
Whole team receives the same eventsNoNoNoYesYes
Request history and logsConsole outputInspector (session)NoNoYes
Filter / transform before deliveryNoLimitedNoNoYes
Also delivers to a private server or KubernetesNoAgent per hostYesNoYes
Exposes a full app, TCPNoYesYesNoYes (tunnels)
Free tierYesYes, limitedYesYesYes

Which one should you use?

  • One provider, one developer, today: the provider CLI.
  • Show someone a local app or expose a whole service: Cloudflare Tunnel, or ngrok if you already have it.
  • A GitHub App experiment: smee.io.
  • Webhooks for a team, several providers, or anything that must keep working next week: Webhook Relay.
  • Just want to see the payload first: a free Webhook Bin, no install, then replay it to localhost.

Provider-specific walkthroughs, each with the exact events and signature header: Stripe, GitHub, GitLab, Twilio, Slack and more in the guides hub.

Troubleshooting

  • The provider reports a timeout or 5xx. Your local server is not answering on the port in the command, or is answering slowly. Check the request log (Webhook Relay) or inspector (ngrok) to see what was forwarded and what came back.
  • Signature verification fails locally. Some tunnels rewrite headers or the body encoding. Verify against the raw body, and use the signature verifier with a captured request to confirm the secret.
  • Events arrive twice. Providers retry on non-2xx. Return 200 quickly and process asynchronously, and make handlers idempotent; see webhook retries and idempotency.
  • The URL changed and the provider is stale. Switch to a tool with a permanent URL so this stops happening.