The Classic Dev Problem
Here's a problem every developer hits when working with webhooks: the service sending the webhook (Stripe, GitHub, whatever) needs to make an HTTP request to your server. But during development, your server is running on localhost:3000 — and that's not accessible from the internet.
You can't tell Stripe to POST to http://localhost:3000/webhooks/stripe. That URL means nothing to Stripe's servers. It would be like giving someone directions to "my house" without telling them the address.
You have a few ways to solve this. Let's go through each one.
The 30-Second Start: Watch Requests Arrive Live
If you just want to see what a webhook payload actually looks like before you write any code, the fastest approach is an online inspection tool. You get a unique public URL, point the service at it, and watch the requests arrive in your browser.
Our Webhook Tester does exactly this — you get a URL like https://jsontotable.org/wh/abc123, paste it into Stripe's dashboard, trigger a test event, and the full request shows up — headers, body, everything.
This approach is perfect when you're in the "what does this thing even send?" phase — before you've written a single line of handler code. You just want to see the payload shape, check which headers are included, and confirm the webhook is actually firing. Takes about 30 seconds to set up.
Where it falls short: the request never reaches your actual server, so you can't run or debug your application code this way. Once you know what the payload looks like and you're ready to wire things up properly, move on to ngrok or the Stripe CLI below.
For Running Real Code: Tunnel with ngrok
ngrok creates a secure tunnel from a public URL to your local machine. When the webhook arrives at the public URL, ngrok forwards it to your local server. Your code runs, just like it would in production.
Install ngrok
# macOS brew install ngrok # Windows (with Chocolatey) choco install ngrok # Or download directly from ngrok.comStart your local server
# Whatever command starts your server npm run dev # Server running on http://localhost:3000Run ngrok
ngrok http 3000ngrok gives you a URL like https://abc123.ngrok.io. That's your public URL.
Register the URL with your service
In Stripe's dashboard (or wherever), set the webhook URL to something like:
https://abc123.ngrok.io/webhooks/stripeNow trigger a test event. ngrok forwards it to localhost:3000/webhooks/stripe, your code runs, and you can debug normally.
ngrok also has a local dashboard
Open http://localhost:4040 while ngrok is running. You'll see every request that came through, with full headers and body — and you can replay them with one click. Very useful when debugging.
If You're on Stripe: Their CLI is Actually Better
Some services have their own CLI tools that handle the tunneling for you. Stripe is the best example — the Stripe CLI is great for webhook testing and worth installing if you're doing any Stripe integration work.
# Install Stripe CLI brew install stripe/stripe-cli/stripe # Login to your Stripe account stripe login # Forward events to your local server stripe listen --forward-to localhost:3000/webhooks/stripe # In another terminal, trigger a test event stripe trigger payment_intent.succeeded
The Stripe CLI handles the tunnel, filters only Stripe events, and even automatically injects the correct webhook signing secret so your signature verification works locally without any config changes.
If you're only working with Stripe, the CLI is honestly the better choice — it handles signatures automatically, lets you filter to specific event types, and the stripe trigger command saves a lot of time. ngrok is better when you're juggling multiple services or need a general-purpose tunnel that isn't tied to one platform.
No Account Needed: localtunnel
If you don't want to sign up for ngrok, localtunnel is a quick free alternative. It's less reliable for long-running sessions, but fine for a quick test.
# Install npm install -g localtunnel # Expose port 3000 lt --port 3000 # Output: your url is: https://shy-penguin-42.loca.lt
Use the URL it gives you as your webhook endpoint. It works the same way as ngrok — incoming requests get forwarded to your local port.
A Few Things Worth Knowing
A couple of habits that will save you a lot of head-scratching during webhook development:
Log the raw request first. Before you do any processing, dump the full headers and body to the console. You'll spend a lot less time guessing what came in when something breaks.
app.post('/webhooks', (req, res) => {
console.log('Headers:', req.headers);
console.log('Body:', req.body);
// actual logic below
});Save a real payload as a fixture file. Once you've captured a legitimate webhook, save it to a test-fixtures/ folder. Then you can replay it instantly with curl — no tunnel, no live service, no waiting:
curl -X POST localhost:3000/webhooks/stripe \ -H "Content-Type: application/json" \ -d @test-fixtures/payment-succeeded.json
Test what happens when things go wrong. Return a 500 from your handler. Let it time out. See what the retry behavior looks like. Most services will retry a failed webhook several times with exponential backoff — you want to understand that before it happens in production with real transactions.
ngrok URLs change every restart on the free tier. If you've registered the URL in a service's dashboard, you'll need to update it each time. Worth knowing before you spend 20 minutes wondering why webhooks stopped arriving.
Which Should You Use?
| Tool | Best for | Free tier |
|---|---|---|
| Webhook Tester (this site) | Inspecting payloads, no code needed | Yes |
| ngrok | General local development tunnel | Yes (random URL) |
| Stripe CLI | Stripe integrations specifically | Yes |
| localtunnel | Quick tests, no account signup | Yes (less reliable) |
For most developers: start with our Webhook Tester to understand the payload shape, then switch to ngrok (or your service's CLI) when you're ready to run real application code.
When the Webhook Isn't Arriving
You've set everything up but the requests aren't showing up. Before assuming your code is broken, work through this in order:
Check the service's delivery log first. Stripe, GitHub, and most webhook-capable services keep a log of every delivery attempt — what they sent, what response they got, and whether it succeeded. This is the fastest way to find out if the problem is on their end or yours. GitHub's webhook guide shows exactly where to find this in their UI.
Make sure your tunnel is still running. ngrok and localtunnel connections drop occasionally, especially after your laptop sleeps. If your tunnel died, the service is successfully sending the webhook — it's just hitting a dead URL. Check your terminal window.
Check your handler isn't rejecting the request. If you have signature verification set up, a mismatch will cause your handler to return a 4xx before it does anything useful. Stripe's signature verification docs explain the exact format — a single extra byte in the body will break the HMAC check. When debugging, temporarily skip signature verification to confirm that's not the issue, then re-enable it.
Confirm your URL is correct and public. Paste your ngrok URL into a browser. If it loads something (even an error page), it's reachable. If it times out, ngrok isn't running or your local server is down.
Look for silent failures. If your handler returns 200 but nothing happens, the webhook is arriving fine — the bug is in your application logic. Add a log right at the top of your handler (before any processing) to confirm you're receiving it, then work down from there.