← Back to Blog
Guide webhooks localhost development ngrok debugging

How to Forward Webhooks to Localhost for Local Development

Receiving a Stripe or GitHub webhook on your local machine is one of those developer rites of passage. There are a few different ways to do it — here's what each one involves and which works best in practice.

JW

Jason Warner

March 17, 2026

How to Forward Webhooks to Localhost for Local Development

Your local server doesn't have a public IP. Stripe, GitHub, Shopify — none of them can reach http://localhost:3000. But you need to test your webhook handler with real events during development. Here's every practical way to solve that problem.

Option 1: ngrok

ngrok is the most common solution. It creates an encrypted tunnel from a public URL to a port on your local machine.

# Install
brew install ngrok

# Start a tunnel to your local port
ngrok http 3000

ngrok prints a public URL like https://abc123.ngrok.io. Give that to Stripe, GitHub, or whatever service you're testing. Requests to that URL get forwarded to localhost:3000.

The free tier works well for basic use. The ngrok dashboard at http://localhost:4040 shows recent requests and lets you replay them, which is useful for testing.

Limitations:

  • Free tier generates a new random URL each time you restart ngrok. You need to update your webhook URL in every service's dashboard every time.
  • Paid tier gives you a stable subdomain.
  • You need to keep the ngrok process running. If it dies, webhooks stop arriving.
  • This only works while you're actively developing — not suitable for a staging environment.

Option 2: Stripe CLI (Stripe-Specific)

If you're working with Stripe, their CLI has a dedicated webhook forwarding command:

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

This creates a temporary webhook endpoint in your Stripe account and forwards events to your local server. It also automatically handles signature verification, which is convenient.

Limitations: Stripe-specific. Doesn't help with GitHub webhooks, Shopify, or anything else.

Option 3: Cloudflare Tunnel

Cloudflare Tunnel (formerly Argo Tunnel) creates a persistent tunnel from your local machine to a Cloudflare-managed URL. It's more reliable than ngrok for long-running tunnels and has a generous free tier.

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

If you already use Cloudflare, this integrates cleanly with your infrastructure.

Option 4: Bluejay Relay + ngrok Together

Bluejay Relay and ngrok complement each other well. You point the webhook sender (Stripe, GitHub, etc.) at your Bluejay Relay capture URL permanently — even in production. During local development, you run ngrok and add your ngrok public URL as a destination in Bluejay Relay.

Stripe -> Bluejay Relay capture URL -> ngrok public URL -> localhost:3000/webhook

Bluejay Relay can't reach localhost directly — it's a cloud service with no path to your machine. But it can reach your ngrok public URL, and ngrok bridges that to your local port.

Why this combination is better than ngrok alone:

  1. The webhook URL never changes. You configure Stripe once with your Bluejay Relay URL. When you restart ngrok and get a new URL, you update the Destination in Bluejay Relay only — not Stripe. No dashboard-hopping.

  2. Full delivery log. Every webhook that arrives is logged in Bluejay Relay with the full payload. If ngrok was down and you missed an event, you can replay it from the log once ngrok is back up.

  3. Replay from the UI. Made a code change? Click Replay in the log to resend any previous payload. No need to trigger the event again from the source.

  4. Works across environments. Add your staging URL and production URL as separate destinations alongside your ngrok URL. Toggle destinations on/off as you move between environments.

Setup:

  1. Start ngrok: ngrok http 3000 — copy the public URL (e.g., https://abc123.ngrok.io)
  2. Create an Integration in Bluejay Relay and copy the capture URL
  3. Add the capture URL to Stripe (or GitHub, or wherever) as your webhook endpoint
  4. Add your ngrok URL (e.g., https://abc123.ngrok.io/webhook) as a Destination in Bluejay Relay
  5. Send a test event — it flows: Stripe → Bluejay Relay → ngrok → localhost

When you restart ngrok and get a new public URL, just update the Destination in Bluejay Relay. Your Stripe webhook config stays the same forever.

Comparison

Method Stable URL Works in Production Delivery Logs Replay Multi-destination
ngrok (free) No No Basic Yes No
ngrok (paid) Yes No Basic Yes No
Stripe CLI No No No No No
Cloudflare Tunnel Yes Yes No No No
Bluejay Relay + ngrok Yes Yes Full, permanent Yes Yes

Recommended Setup for Most Teams

For quick local testing: ngrok alone is fast and requires no setup beyond installation. Use it.

For a more robust development workflow: use Bluejay Relay as the permanent webhook endpoint with ngrok as the local bridge. Point your webhook sources at your Bluejay Relay URL once and never touch that config again. When your ngrok URL changes, update only the Bluejay Relay destination. Add staging and production URLs as separate destinations — toggle them on/off as you move between environments.


Bluejay Relay is free to start — the free tier covers most local development scenarios.

#webhooks #localhost #development #ngrok #debugging

Receive webhooks without a server.

Capture, store, and forward webhooks from a hosted intake URL — nothing to deploy or keep running. Free to start.