Test Webhooks Locally
The webhook testing problem
Webhooks are HTTP POST requests that external services send to your server when events occur — a payment succeeds, a pull request opens, an order ships. Your local dev server listens on localhost, which is unreachable from the internet. Without a public URL, providers cannot deliver events to your machine.
Two proven approaches solve this: provider CLI tools that forward events locally, and HTTPS tunnels that give providers a real URL to POST to.
Pick a provider below — each linked guide opens with provider-specific verification steps so you do not repeat this hub verbatim.
Two approaches compared
| Approach | How it works | Best when |
|---|---|---|
| Provider CLI | Tool receives events and forwards to localhost | Quick iteration, signature testing |
| HTTPS tunnel | Public URL forwards to your local handler | Testing exact production URL format, dashboard "Send test webhook" |
Both are valid. Many teams use the CLI for day-to-day development and a tunnel when validating the full integration path.
Quick start with LocalToLink
1. Start your app with a webhook handler route (e.g. /api/webhooks/stripe):
npm run dev
2. Start the tunnel in a second terminal:
npx localtolink --port 3000
3. Register the webhook URL in your provider's dashboard:
https://k7m2x9p4q1.localtolink.com/api/webhooks/stripe
Replace k7m2x9p4q1 with the 10-character subdomain LocalToLink assigns.
4. Trigger an event from the provider (test button, real action, or CLI) and watch your local logs.
Some automated clients may reach your app differently than a first-time browser visit. Webhook delivery is server-to-server — no special headers needed for normal provider POSTs.
Provider-specific guides
Each provider has signature verification rules and dashboard quirks. Follow the dedicated guide for your stack:
| Provider | Guide | Typical webhook path |
|---|---|---|
| Stripe | Test Stripe Webhooks Locally | /api/webhooks/stripe |
| GitHub | Test GitHub Webhooks on Localhost | /api/github/webhook |
| Shopify | Test Shopify Webhooks in Development | /webhooks/shopify |
| Twilio | Test Twilio Webhooks Locally | app route twilio/voice |
| n8n | n8n OAuth and Webhooks on Localhost | /webhook/your-path |
When a tunnel is not needed
Some providers ship local forwarding tools that avoid a public URL entirely:
Stripe CLI — forwards events directly to localhost:
stripe listen --forward-to localhost:3000/api/webhooks/stripe
The CLI prints a signing secret (whsec_...) for local verification. This is the fastest path for Stripe-only development.
GitHub — no official local forwarder. A tunnel is usually simpler than proxy hacks.
Use the provider CLI when you only need to verify handler logic. Use a tunnel when you need the exact URL format your production deployment will use.
Tips for reliable webhook testing
- Use
--portif multiple apps run locally — auto-detection may pick the wrong server - Use
--jsonin CI to capture the URL programmatically - Log payloads in your handler — LocalToLink has no request inspector (unlike ngrok)
- Verify signatures — always validate provider signatures; never trust raw POST bodies
- Check provider delivery logs — Stripe Events, GitHub Recent Deliveries, Shopify webhook logs
- Sessions last 2 hours — restart the tunnel and update dashboard URLs if testing longer
- Rate limit is 10 req/s — sufficient for normal webhook traffic; not for load testing
Webhook handler checklist
Before registering a URL, confirm your handler:
- Accepts POST requests on the registered path
- Reads the raw body (required for signature verification)
- Returns 2xx quickly (providers retry on failure)
- Validates signatures using the provider's documented method
- Is idempotent (providers may deliver the same event more than once)
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Provider shows delivery failure | Handler returned 4xx/5xx or timed out | Check local logs; return 200 after queuing work |
| Signature verification fails | Body parsed before verification | Use raw body middleware; verify before JSON parse |
| No events arrive | Wrong URL or expired tunnel | Confirm subdomain matches; restart if session expired |
| Request blocked by tunnel | Session or usage limit may apply | Restart tunnel; reduce test volume |
| Events arrive but handler silent | Wrong port | Use --port to match your dev server |
Free tier limits relevant to webhooks
| Limit | Impact |
|---|---|
| 2-hour session | Restart tunnel for long test sessions |
| 1 GB bandwidth | Plenty for webhook payloads |
| 10 req/s | Normal webhook volume is fine |
| Random subdomain | Update provider URL on each new session |
| No request replay | Use provider dashboards to resend events |
FAQ
Do webhooks hit the browser warning page?
Webhook POSTs are server-to-server. Some automated clients may reach your app differently than a first-time browser visit in a browser.
Can I test multiple providers at once?
One tunnel per IP on the free tier. Route multiple paths on the same tunnel hostname (for example api/webhooks/stripe and api/github/webhook on your assigned subdomain).
What if my subdomain changes?
Update the webhook URL in each provider dashboard. There is no persistent URL on the free tier.
Should I use ngrok instead?
If you need a visual request inspector and one-click replay, ngrok is better. For quick anonymous testing, LocalToLink works without account signup. See LocalToLink vs ngrok.
For tunnel basics, see Expose Localhost to the Internet.
Try it now
Start a tunnel from your project directory — no account required.
$ npx --yes localtolink@latest