To test stripe webhooks locally, use Stripe CLI first. It forwards sandbox events to your local webhook route, gives you the signing secret for local verification, and can trigger common test events from your terminal.
A localhost tunnel still has a place. Use it when you need to show the full payment flow to a reviewer while Stripe sends callbacks into the local app. That review context needs a browser link, a webhook route, and clear failure signals.
Test stripe webhooks locally with Stripe CLI
Start your app and make sure the webhook route accepts POST requests. For example, your local route might be:
http://localhost:4242/webhook
Then run Stripe CLI:
stripe listen --forward-to localhost:4242/webhook
Stripe prints a webhook signing secret. Use that value in your local environment for signature verification:
STRIPE_WEBHOOK_SECRET=whsec_test_value npm run dev
Trigger a test event:
stripe trigger payment_intent.succeeded
If the handler returns a success status, Stripe CLI shows the local delivery. If the handler fails, fix the route before you involve a browser demo or external reviewer.
Build the handler around Stripe’s rules
Stripe webhook handlers need a few details that ordinary JSON API routes can miss.
First, preserve the raw request body for signature verification. Stripe signs the raw payload. If your framework parses and rewrites the body before verification, the signature check can fail.
Second, return a 2xx response before slow work. Stripe’s docs recommend returning a success status before complex processing that could time out. Put fulfillment, email, or sync work into a queue when the work can outlive the request.
Third, handle duplicate events. Stripe may deliver the same event more than once, so store processed event IDs or use the event type plus object ID when needed.
Fourth, do not assume order. Stripe does not guarantee event ordering. Retrieve missing objects from the API when your state machine needs the current object.
Filter the events you need
Forwarding every event creates noise. Stripe CLI lets you filter to the event types your handler supports:
stripe listen \
--events payment_intent.succeeded,checkout.session.completed,payment_intent.payment_failed \
--forward-to localhost:4242/webhook
This keeps local logs readable and helps you catch routes that subscribe to too much. In production, Stripe also recommends listening only to event types your integration needs.
When a localhost tunnel helps
Stripe CLI is the best first tool for local Stripe handler work. A tunnel helps when Stripe webhooks are part of a live local product flow.
Consider a client review where you need to show checkout, redirect handling, and a local admin state update. The reviewer opens a browser link. You run through the checkout. Stripe sends the webhook. You need to see whether the callback reached the local app and whether the UI changed.
Start wiremaven for the app server:
npx wiremaven-cli 3000 --expires 30m --name stripe-demo
Then use the generated public URL for the visible app review. If your app exposes the webhook path on the same server, you can register the temporary URL in a Stripe sandbox endpoint for the review window:
https://example.wiremaven.dev/webhook
Use this with care. It is a temporary public endpoint, so keep the route in sandbox mode, verify signatures, and remove or let the endpoint expire after the review.
wiremaven’s value here is session visibility. It shows live viewer, request, and failure signals during the walkthrough. The link uses a time-boxed TTL, and reviewers do not need an account during beta.
Stripe CLI vs. tunnel
| Job | Use Stripe CLI | Use a tunnel |
|---|---|---|
| Build the handler | Yes | Optional |
| Get local signing secret | Yes | No |
| Trigger test events | Yes | No |
| Show full local app to reviewer | No | Yes |
| Test callback over public HTTPS URL | Sometimes | Yes |
| See browser reviewer state | No | Depends on tool, yes in wiremaven |
For most developers, the answer is both at different stages. Use Stripe CLI while you build. Use a temporary tunnel when the webhook must participate in a visible local review.
Common local failures
- Your route accepts
GETbut notPOST. - Your framework consumes the raw body before signature verification.
- Your handler returns a redirect.
- The handler takes too long before returning
2xx. - The signing secret comes from the wrong endpoint.
- You test live mode behavior with sandbox credentials or the reverse.
Stripe’s delivery view and CLI output help with provider-side status. A local tunnel with request signals helps with review-side status when someone opens the app through a temporary link.
FAQ
Do I need ngrok to test Stripe webhooks locally?
No. Stripe CLI can forward sandbox events to a local endpoint without ngrok. A tunnel helps when you need a public HTTPS URL for a broader local demo.
Can Stripe send webhooks to localhost directly?
No. Stripe cannot reach localhost on your machine from the internet. Stripe CLI bridges sandbox events to localhost, and a tunnel can expose a temporary public route.
Why does Stripe signature verification fail locally?
The common cause is body parsing before verification. Stripe requires the raw request body plus the Stripe-Signature header and endpoint secret.
Should I register a temporary tunnel URL in Stripe?
Only for sandbox testing or a scoped review. Keep the endpoint temporary, verify signatures, and remove the endpoint when the session ends if you registered it in the dashboard.
Start with CLI, then add review visibility
Build the handler with Stripe CLI:
stripe listen --forward-to localhost:4242/webhook
When the full local app needs a review link, use wiremaven:
npx wiremaven-cli 3000 --expires 30m --name stripe-demo
Read the wiremaven docs, review how wiremaven works, and compare the broader tradeoffs in webhook proxy vs. localhost tunnel.
Related: Webhook Proxy vs. Localhost Tunnel | 7 ngrok Alternatives for Developers in 2026