How to Test Webhooks Locally: Complete 2026 Guide
How to Test Webhooks Locally
Testing webhooks during local development is one of those problems that sounds simple until you actually try to do it. Your localhost isn't accessible from the internet, so services like Stripe, GitHub, or Shopify can't reach your development server.
Here's the thing: there are several ways to solve this, and the right choice depends on your situation.
The Core Problem
When Stripe wants to notify you about a successful payment, it sends an HTTP POST request to your webhook URL. In production, that's straightforward—your server has a public URL.
But during development? Your app is running on localhost:3000. Stripe can't reach that. Neither can any other webhook provider.
Solution 1: Tunneling Tools
The most common approach is creating a tunnel from the public internet to your local machine.
ngrok
ngrok is the go-to tool for this. Install it, run ngrok http 3000, and you get a public URL like https://abc123.ngrok.io that forwards to your local server.
ngrok http 3000
The free tier works, but URLs change every time you restart. That means updating your webhook configuration in Stripe/GitHub/wherever constantly. The paid tiers ($8-25/month) give you stable URLs.
Cloudflare Tunnel
If you're already using Cloudflare, their Tunnel product (formerly Argo Tunnel) is solid. It's free and integrates with your existing Cloudflare setup.
cloudflared tunnel --url http://localhost:3000
localtunnel
localtunnel is an open source alternative. Simple to use, but reliability can be hit or miss:
npx localtunnel --port 3000
Solution 2: Webhook Capture Services
Instead of tunneling, you can use a service that captures webhooks for you to inspect and replay. Think of it as an inbox for your webhooks.
This solves problems that tunneling can't:
- Webhooks arrive at 3am? Captured and waiting in your dashboard. Your laptop can be closed.
- Need to replay the same webhook 20 times while debugging? No problem. No need to trigger real events in Stripe repeatedly.
- Tired of updating webhook URLs every time ngrok restarts? Set the URL once, never touch it again.
- Want to see exactly what Stripe sends before writing code? Inspect headers, body, and timing first.
How it works
- You get a permanent URL (e.g.,
https://thunderhooks.com/h/my-project) - Configure Stripe/GitHub/etc. to send webhooks there—once, and forget it
- Webhooks are captured and stored (typically 7-30 days)
- When you're ready to code, open the dashboard and inspect the payload
- Start a tunnel, then replay the webhook to your tunnel URL
- Bug in your code? Fix it and replay the same webhook again
Why this beats tunneling alone
With ngrok, you need the tunnel running when the webhook arrives. Miss it and it's gone—you have to trigger another real event.
With a capture service, webhooks queue up like emails. Process them when you're ready. Replay them as many times as needed. Your development workflow isn't tied to keeping a tunnel alive 24/7.
The tradeoff: it's a two-step process (capture → replay) instead of direct forwarding. But for most webhook development, that tradeoff is worth it.
Solution 3: Provider-Specific Tools
Some webhook providers have their own testing tools.
Stripe CLI
Stripe's CLI can forward webhook events to your local server:
stripe listen --forward-to localhost:3000/webhooks/stripe
It also lets you trigger test events:
stripe trigger payment_intent.succeeded
This is excellent for Stripe-specific development. The limitation is obvious—it only works for Stripe.
GitHub CLI
GitHub doesn't have webhook forwarding built-in, but you can use their API to redeliver webhooks from the settings page.
Practical Workflow
Here's what actually works well day-to-day:
For quick debugging: Use a capture service. See what's being sent, inspect headers and payloads, then replay to your local server.
For integration testing: Use the Stripe CLI (or equivalent) to trigger specific events and verify your handling code.
For end-to-end testing: Set up a tunnel with a stable URL so real webhook events flow through during testing.
Common Mistakes
Not verifying signatures: In development, it's tempting to skip signature verification. Don't. You'll forget to enable it in production. See Stripe's signature verification docs for implementation details.
Hardcoding URLs: Store webhook URLs in environment variables. Your local, staging, and production environments need different URLs.
Ignoring idempotency: Webhooks can be delivered multiple times. Your handler needs to handle duplicates gracefully. Test this by replaying the same webhook twice.
Missing timeout handling: Webhook providers expect quick responses (usually under 30 seconds). If your handler does heavy processing, return 200 immediately and process asynchronously.
Testing Checklist
Before deploying webhook handlers to production:
- Handler returns 2xx status quickly
- Signature verification is enabled
- Duplicate deliveries are handled (idempotency)
- Failures are logged with enough context to debug
- Retry logic handles temporary failures
- Timeout handling works correctly
Conclusion
The webhook testing landscape has improved a lot. Between tunneling tools, capture services, and provider-specific CLIs, you have options.
Pick the approach that fits your workflow. For most teams, a combination works best: capture services for debugging and inspection, provider CLIs for triggering test events, and tunnels for full integration testing.