How to Debug Stripe Webhooks
Stripe webhooks are unforgiving: one missing header, one wrong parser, one environment mismatch — and events silently disappear. Here's how to actually see what's going wrong.
Use our webhook debugger →Common Stripe webhook issues
1. Works locally but fails in production
Classic. Locally you're using the Stripe CLI forwarder (stripe listen) which signs events with a different webhook secret than your production endpoint. Your production code uses the wrong secret and rejects the request as unverified.
Fix: Each endpoint (local, staging, production) has its own whsec_... secret. Store them as separate environment variables and make sure the code uses the right one per environment.
2. Signature mismatch (No signatures found matching the expected signature)
Almost always caused by body middleware mutating the payload before Stripe's library gets to verify it. Stripe signs the raw request body byte-for-byte. If Express has already parsed it into a JavaScript object and re-stringified it, the signature no longer matches.
Fix: Use express.raw({ type: 'application/json' }) only on the Stripe webhook route, before any express.json() middleware:
app.post('/stripe-webhook', express.raw({ type: 'application/json' }), handler);
3. Missing or truncated payload
Reverse proxies (Nginx, CloudFront, API Gateway) can strip or buffer request bodies if limits are too low, or if they're set to rewrite content. The result: Stripe's payload arrives empty or cut off.
Fix: Bump client_max_body_size in Nginx, disable any body-rewriting filters, and confirm with a raw inspection tool — like this webhook debugger — that the full payload actually reaches your server.
4. Events firing but handler never runs
Usually an async/error-handling bug: your handler throws, Express returns 500, Stripe retries (up to 3 days), and nothing visible tells you why. Check your logs and verify you return 200 quickly.
Debugging steps
- Generate a fresh webhook URL here. Click the Generate button on the homepage — you'll get a unique endpoint.
- Register it in Stripe. Go to Stripe Dashboard → Developers → Webhooks → Add endpoint, paste the URL, select the events you're debugging.
- Trigger an event. Use Stripe's "Send test webhook" button, or run a real test charge.
- Inspect the payload. The debugger shows you the exact method, headers (including
stripe-signature), and raw JSON body Stripe actually sent. - Compare with what your app receives. If your app sees something different, the problem is in your stack (middleware, proxy, framework) — not in Stripe.
- Replay as needed. Use the replay button to re-send the same payload while iterating on fixes.
Why seeing the raw payload matters
When Stripe's library rejects a signature, the error message tells you that it failed — not why. You can't fix what you can't see. By routing the webhook through a tester first, you confirm:
- The full, untouched
stripe-signatureheader is present. - The raw body bytes match what Stripe says they should.
- The content-type is
application/jsonas expected. - Nothing in the middle (proxy, WAF, CDN) is mutating the request.
Once you see the real request, the bug is almost always obvious within minutes.
Use our webhook debugger →