Back to Blog
guides8 min readApril 12, 2026

How to Test Webhooks Locally in 2026 (4 Methods Compared)

Your webhook handler runs on localhost. The provider needs a public HTTPS URL. Something has to bridge that gap. This guide compares every approach — cloud relay, ngrok, provider CLIs, and direct payload testing — so you can pick the right one and start receiving real events locally in minutes.

A
Abinash B
April 12, 2026

The Problem Every Webhook Developer Faces

You've written a webhook handler. It looks right. But there's no way to know it actually works until a real provider — Stripe, GitHub, Shopify, Slack — sends an HTTP POST to a publicly reachable URL. And that URL is not localhost:3000.

This is the webhook testing problem. Your code is local. The provider is not. Something has to bridge the gap.

This guide covers every approach to testing webhooks locally in 2026 — from the quick-and-dirty to the production-ready — and helps you pick the right one for your situation.


Option 1 — Cloud Relay (Recommended for Most Developers)

A cloud relay gives you a permanent public HTTPS URL that forwards incoming webhook events to your local server. No tunnel, no binary to install, no rotating URLs.

How it works:

  1. You get a permanent public URL (e.g. https://webhookwhisper.com/hook/abc123)
  2. You register that URL with your provider — Stripe, GitHub, Shopify — once
  3. You set a forwarding rule: relay all incoming events to http://localhost:3000/webhooks
  4. Events arrive at your local handler with all original headers intact

Why this beats ngrok: The public URL never changes. You register it once and forget it. Your local server can restart freely without breaking the integration. If your server is down, events are queued and retried. The full payload and headers are logged so you can inspect and replay any event.

How to set it up with WebhookWhisper:

  1. Go to webhookwhisper.com and click Get my free webhook URL
  2. Register the URL with your provider (see provider-specific guides below)
  3. Open the Forwarding tab, add a rule: target = http://localhost:3000/webhooks
  4. Fire a test event — it arrives at your handler in real time

Works for every major provider: Stripe, GitHub, Shopify, Slack, and 35+ more.


Option 2 — ngrok (The Classic Tunnel)

ngrok creates a tunnel from a public URL to a port on your machine. It's the most widely known solution, but has real friction:

  • Rotating URLs on free plan — every restart gives you a new URL, meaning you must re-register the webhook in every provider
  • Persistent terminal process — the tunnel dies when you close the window
  • Binary to install and update — one more tool in the chain
  • Exposes your whole local port — not just the webhook path
  • No event persistence — if your server was down when an event arrived, it's gone

ngrok is fine for a quick one-off test. It's painful for day-to-day webhook development. See our full webhook forwarding guide for a direct comparison.

# ngrok basic usage
ngrok http 3000

# Your public URL (changes every restart on free plan):
# https://a1b2c3d4.ngrok.io → http://localhost:3000

Option 3 — Provider CLI Tools

Some providers ship their own CLI for local webhook testing:

Stripe CLI

# Install
brew install stripe/stripe-cli/stripe

# Authenticate
stripe login

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

Works well but only for Stripe events. Requires auth, a running terminal process, and the Stripe CLI binary. The signing secret it uses is CLI-specific — different from your Dashboard endpoint secret. Read our Stripe webhook testing guide for the full setup.

GitHub CLI / Smee.io

GitHub doesn't ship a native forwarding CLI. The community tool is smee.io — a free proxy that uses Server-Sent Events. It works but has the same rotating URL problem as ngrok free tier.


Option 4 — Fire Test Payloads Directly (No Real Provider Needed)

For many webhook handler tests, you don't need a real event from the provider at all. You just need a realistic payload hitting your endpoint. There are two ways to do this:

WebhookWhisper Test Sender (No account needed)

Go to any provider page in our webhook provider directory, select an event type, enter your localhost URL, and click Send. A realistic payload with proper headers fires at your endpoint from our servers — bypassing the browser CORS restriction and reaching your local handler directly.

Works for all 35+ providers without any account or provider credentials. See it in action:

curl / Postman

For simple handlers, you can POST a crafted payload directly:

curl -X POST http://localhost:3000/webhooks/stripe \
  -H "Content-Type: application/json" \
  -H "Stripe-Signature: t=1712000000,v1=test" \
  -d '{"type":"payment_intent.succeeded","data":{"object":{"amount":2999}}}'

Limitation: the signature won't verify (since you don't know the signing secret for a locally crafted payload). Use this only for testing handler routing logic, not signature verification.


Provider-Specific Guides

Each provider has quirks — different signature headers, different retry policies, different event formats. Use these guides for provider-specific setup:

ProviderGuideKey Header
StripeTesting guide · Signature verificationStripe-Signature
GitHubTesting guideX-Hub-Signature-256
ShopifyTesting guideX-Shopify-Hmac-Sha256
SlackEvents referenceX-Slack-Signature
35+ moreProvider directoryVaries

Verifying Webhook Signatures Locally

Most providers sign their webhooks with HMAC-SHA256. Verifying the signature locally is the same as in production — your signing secret doesn't change based on environment. The only difference is where the event comes from.

The critical rule across all providers: use the raw request body, never the parsed JSON object. In Node.js:

// Express — webhook route must use raw body middleware
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  // req.body is a Buffer here — pass directly to signature verification
  const isValid = verifySignature(req.body, req.headers)
  if (!isValid) return res.status(401).send('Unauthorized')

  const payload = JSON.parse(req.body) // parse AFTER verification
  // handle event...
  res.json({ received: true })
})

For provider-specific signature verification code, see our Stripe signature verification guide (covers Node.js, Python, and Go).


Debugging Webhooks That Fail Locally

When your local handler returns an error or your test event never arrives, follow this checklist:

  1. Check the provider's delivery log first — did the event leave the provider? Stripe, GitHub, and Shopify all have delivery attempt logs in their dashboards.
  2. Check WebhookWhisper's event log — did it arrive at the public endpoint? If yes and your handler didn't receive it, the forwarding rule is the issue.
  3. Check your local server is running — it sounds obvious but forwarding silently fails if nothing is listening on the target port.
  4. Check the response code — the delivery log shows what HTTP status your handler returned. A 500 means your code crashed; a 401 usually means signature verification failed.
  5. Replay the event — fix the bug, click Replay in WebhookWhisper, confirm the handler now returns 200.

For a deep dive into each failure mode, read How to Debug Webhooks: A Practical Guide.


Choosing the Right Approach

SituationBest Option
Day-to-day webhook development with any providerCloud relay (WebhookWhisper)
Quick one-off test, don't mind rotating URLngrok free tier
Stripe-only integration, comfortable with CLIStripe CLI listen
Testing handler logic without real providerTest Sender or curl
Testing signature verification end-to-endCloud relay + Dashboard "Send test event"
System behind firewall, can't receive inboundPolling + reconciliation job

Summary

Testing webhooks locally comes down to one problem: your handler is not publicly reachable. The solutions, in order of recommendation:

  1. Cloud relay (WebhookWhisper) — permanent URL, full logging, forwarding with retry, works for all 35+ providers
  2. ngrok — fast to start, rotating URL is friction for daily use
  3. Provider CLI — only works for one provider at a time
  4. Direct curl/Test Sender — for handler logic tests without a real provider event

Related Guides

#webhooks#localhost#testing#ngrok#tutorial

Ready to test your webhooks?

Get a free HTTPS endpoint in under 5 seconds — no signup required.

Create Free Account
How to Test Webhooks Locally (2026) — 4 Methods Compared | WebhookWhisper