Webhooks

How to Test Webhooks Locally

The classic problem: Stripe wants to POST to your server, but your server is on your laptop. Here's how to fix that.

April 20258 min read

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.

1

Install ngrok

# macOS brew install ngrok # Windows (with Chocolatey) choco install ngrok # Or download directly from ngrok.com
2

Start your local server

# Whatever command starts your server npm run dev # Server running on http://localhost:3000
3

Run ngrok

ngrok http 3000

ngrok gives you a URL like https://abc123.ngrok.io. That's your public URL.

4

Register the URL with your service

In Stripe's dashboard (or wherever), set the webhook URL to something like:

https://abc123.ngrok.io/webhooks/stripe

Now 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?

ToolBest forFree tier
Webhook Tester (this site)Inspecting payloads, no code neededYes
ngrokGeneral local development tunnelYes (random URL)
Stripe CLIStripe integrations specificallyYes
localtunnelQuick tests, no account signupYes (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.

Keep Reading