Why Testing Stripe Webhooks Locally Is Painful
You've wired up your payment handler. The code looks right. But there's no way to know for sure until a real Stripe event hits your endpoint — and that means deploying to a publicly reachable server every time you need to test a change.
The traditional workarounds all have costs:
- Deploy to staging — slow iteration loop, burns cloud resources
- Stripe CLI — requires installing a binary, authenticating, running a persistent terminal process with a command you always forget
- ngrok — rotating URLs on free plan, CLI to manage, tunnels your entire localhost
- Copy-paste JSON — manually constructing test payloads by hand, missing headers, no signature
There's a better way. This guide covers the fastest path from zero to a working Stripe webhook test loop — with real payloads, real headers, and localhost forwarding.
What You Need Before You Start
- A Stripe account (test mode is fine — you don't need live keys)
- A local server running your webhook handler (e.g.
localhost:3000) - A WebhookWhisper account — free to sign up, or use the guest mode with no account at all
That's it. No binary installs, no CLI setup.
Step 1 — Get a Public Webhook URL
Stripe needs a publicly reachable HTTPS URL. Go to webhookwhisper.com and click Create Free Endpoint. You'll get a URL like:
https://webhookwhisper.com/hook/abc123xyz
This is your webhook receiver. It's live immediately — no configuration, no account required for the guest mode.
If you want a permanent URL that doesn't expire (you'll add it to the Stripe dashboard once and leave it), sign up for a free account and create a named endpoint under the Dashboard.
Step 2 — Add the URL to Stripe
In the Stripe Dashboard:
- Go to Developers → Webhooks
- Click Add endpoint
- Paste your WebhookWhisper URL in the Endpoint URL field
- Under Events to send, either select specific events or choose Select all events
- Click Add endpoint
Stripe will immediately send a test ping. You'll see it arrive in the WebhookWhisper inspector within a second.
Important: Copy the Signing secret shown after you create the endpoint (it starts with whsec_). You'll need this for signature verification in Step 5.
Step 3 — Fire Test Payloads
You don't need to process a real payment to test your webhook handler. There are two ways to fire test events:
Option A — Stripe Dashboard "Send test event"
In the Stripe Dashboard, open your webhook endpoint and click Send test event. Choose an event type from the dropdown (e.g. payment_intent.succeeded) and click Send. Stripe fires a realistic payload at your endpoint with a valid signature.
Option B — WebhookWhisper Test Sender
In WebhookWhisper, go to the Test tab (or use the Stripe webhook testing page). Select Stripe as the provider, choose an event type, and click Send. The payload arrives at your endpoint instantly — no Stripe account action required. This is the fastest way to iterate on your handler code.
WebhookWhisper includes authentic sample payloads for the most common Stripe events (see the full Stripe webhook reference):
| Event | Use case |
|---|---|
payment_intent.succeeded | Fulfil order, send receipt email |
payment_intent.payment_failed | Notify user, trigger retry |
customer.subscription.created | Provision access, onboarding |
customer.subscription.deleted | Revoke access, offboarding |
invoice.payment_succeeded | Update billing record |
invoice.payment_failed | Dunning, payment retry flow |
checkout.session.completed | Fulfil digital goods |
charge.dispute.created | Alert team, gather dispute evidence |
Step 4 — Forward to Your Local Server
This is where WebhookWhisper beats every other approach. You don't need ngrok or any tunnel — just set a forwarding rule:
- In your WebhookWhisper endpoint, open the Forwarding tab
- Click Add rule
- Set the target URL to
http://localhost:3000/webhooks/stripe(adjust port and path to match your app) - Click Save
Now every Stripe event that arrives at your WebhookWhisper URL is automatically relayed to your local server — with all original headers intact, including the Stripe-Signature header.
If your local server is down or returns an error, WebhookWhisper retries automatically. The delivery log shows the HTTP status your server returned, response body, and round-trip duration for every attempt.
Step 5 — Verify the Stripe Signature
Production Stripe webhook handlers must verify signatures. Here's how to implement it in Node.js:
import Stripe from 'stripe'
import express from 'express'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
const app = express()
// ⚠️ Must use raw body — not JSON-parsed
app.post('/webhooks/stripe',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.headers['stripe-signature']
let event
try {
event = stripe.webhooks.constructEvent(
req.body, // raw Buffer
sig, // Stripe-Signature header
process.env.STRIPE_WEBHOOK_SECRET // whsec_...
)
} catch (err) {
console.error('Signature verification failed:', err.message)
return res.status(400).send(`Webhook Error: ${err.message}`)
}
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object
// fulfil order...
break
case 'customer.subscription.deleted':
// revoke access...
break
default:
console.log(`Unhandled event type: ${event.type}`)
}
res.json({ received: true })
}
)
The critical detail: you must read the raw request body before JSON-parsing it, otherwise the signature check fails. Express's json() middleware will break this — use express.raw() specifically on the webhook route.
When testing with WebhookWhisper's forwarding, the Stripe-Signature header is forwarded intact, so constructEvent() works exactly as it does in production.
Note: WebhookWhisper's built-in Test Sender fires payloads without a real Stripe signature. Use the Stripe Dashboard's "Send test event" button when you specifically need to test signature verification end-to-end.
Step 6 — Inspect and Replay Failed Events
Once events are flowing, WebhookWhisper's inspector becomes your debugging tool:
- Full payload view — see exactly what Stripe sent, including nested objects, before your handler processes it
- Header inspection — verify the
Stripe-SignatureandStripe-Idempotency-Keyheaders are present - Delivery log — see the HTTP status your local handler returned for each event
- Event replay — hit the replay button on any past event to re-send the exact payload to your handler after you've fixed a bug
This last point is the biggest time-saver. Previously, if your handler had a bug and returned 500, you'd have to wait for Stripe to retry (up to 3 days later), or manually trigger the event again. With WebhookWhisper, you fix the bug and replay instantly.
Common Stripe Webhook Mistakes to Avoid
1. Parsing the body before verifying the signature
As covered above — stripe.webhooks.constructEvent() requires the raw body as a Buffer, not a parsed JSON object. This is the #1 source of No signatures found matching the expected signature errors.
2. Not handling duplicate events
Stripe guarantees at-least-once delivery, not exactly-once. If your endpoint returns a 5xx or times out, Stripe retries. Your handler can receive the same payment_intent.succeeded event multiple times. Always check if you've already processed a payment before fulfilling the order — use the payment_intent.id as an idempotency key.
3. Using the wrong signing secret
Stripe creates a separate signing secret for each webhook endpoint. Make sure STRIPE_WEBHOOK_SECRET in your environment matches the secret for the specific endpoint you're testing — not the API secret key (sk_test_...). They're different values.
4. Doing synchronous work in the handler
Stripe times out webhook delivery after 30 seconds. If your handler sends emails, calls external APIs, or does heavy database writes synchronously, you'll hit timeouts and trigger retries. The correct pattern: respond 200 { received: true } immediately, then enqueue the work.
5. Only testing the happy path
Most developers test payment_intent.succeeded and ship. But payment_intent.payment_failed, invoice.payment_failed, charge.dispute.created, and customer.subscription.deleted are where the hard bugs live. Use WebhookWhisper's Test Sender to fire each event type before considering an integration complete.
The Full Test Loop in 30 Seconds
Once everything is wired up, your test loop looks like this:
- Change your webhook handler code
- Restart your local server (
npm run dev) - Click Replay on the last event in WebhookWhisper — or fire a new one from the Test Sender
- Check the delivery log — did your handler return 200?
- Check your app's behaviour — was the order fulfilled? Was the user notified?
- Repeat
No deploy. No staging environment. No waiting for real transactions. Just fast, local iteration on real Stripe payloads.
Related Guides
- How to Debug Webhooks: A Practical Guide
- Forward Webhooks to Localhost Without ngrok
- Stripe Webhook Events & Payload Reference
- Stripe Webhook Testing — Free Tool
Summary
Testing Stripe webhooks doesn't have to mean deploying to production or fighting with CLI tools. The fastest workflow is:
- Get a free WebhookWhisper endpoint (no account needed)
- Register it as a Stripe webhook endpoint
- Add a forwarding rule to your localhost handler
- Fire test events from the Stripe Dashboard or WebhookWhisper's Test Sender
- Inspect payloads and replay failures in the WebhookWhisper dashboard
Create your free WebhookWhisper account and have your first Stripe test event flowing in under a minute.