Stripe: webhook signature verification failing [2026 fix]
Webhook signature mismatch caused by incorrect endpoint secret or missing raw body parsing—use raw request body and correct secret from Dashboard.
Stripe: Webhook Signature Verification Failing [2026 Fix]
TL;DR
Cause: You're passing the parsed JSON body instead of raw request bytes to signature verification, or using the wrong endpoint secret. Fix: Extract the raw request body *before* parsing JSON, and verify you're using the Endpoint Secret (not the API key) from Stripe Dashboard → Webhooks.---
Exact Error Messages from Console Output
``` Stripe::SignatureVerificationError: No signatures found matching the expected signature for payload. ```
``` err: { type: 'StripeSignatureVerificationError', message: 'No signatures found matching the expected signature for payload.' } ```
``` Stripe.Signature.verifyHeader() returned false. timestamp=1704067200, signature_mismatch=true ```
```
Invalid sig_ header value. Expected format: t=<timestamp>,v1=<signature>
```
``` Webhook signature timestamp outside tolerance window. Tolerance: 300 seconds, skew: 1847 seconds ```
---
Broken Code vs. Fix
Problem 1: Parsing Body Before Verification
❌ BROKEN: ```javascript app.post('/webhook', express.json(), (req, res) => { // DON'T DO THIS - body is already parsed const sig = req.headers['stripe-signature']; const endpointSecret = 'whsec_...'; // req.body is a JavaScript object, not raw bytes const event = stripe.webhooks.constructEvent( req.body, // ← WRONG: already parsed sig, endpointSecret ); }); ```
✅ FIXED: ```javascript app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { // Correct: req.body is now a raw Buffer const sig = req.headers['stripe-signature']; const endpointSecret = 'whsec_test_...'; const event = stripe.webhooks.constructEvent( req.body, // ← CORRECT: raw bytes sig, endpointSecret ); res.json({received: true}); }); ```
Problem 2: Using Wrong Secret
❌ BROKEN: ```javascript // This is your API key, NOT the endpoint secret const endpointSecret = process.env.STRIPE_API_KEY; // Results in: "No signatures found matching..." ```
✅ FIXED: ```javascript // Go to Dashboard → Developers → Webhooks → Your endpoint → "Signing secret" const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; // whsec_... ```
Problem 3: Multiple Middleware Parsing the Body
❌ BROKEN: ```javascript // This parses body first, webhook handler gets parsed object app.use(express.json());
app.post('/webhook', (req, res) => { const event = stripe.webhooks.constructEvent( req.body, // Already parsed, signature fails req.headers['stripe-signature'], endpointSecret ); }); ```
✅ FIXED: ```javascript // Apply body parsing selectively app.use(express.json()); // For normal routes
// Webhook BEFORE general JSON parser app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { const event = stripe.webhooks.constructEvent( req.body, // Raw buffer req.headers['stripe-signature'], endpointSecret ); });
app.post('/api/other', (req, res) => { // This one gets parsed JSON as usual }); ```
---
Important Version Notes
constructEvent() requires raw Buffer. Older versions may have different behavior—verify your package version with npm list stripe.express.raw() middleware signature is consistent v4.16+, but if you're on v4.15 or older, consider upgrading or using bodyParser.raw().---
Still Broken? Check These Too
1. Endpoint Secret Mismatch Across Environments
You copied the secret from *test mode* but your webhook is firing in *live mode* (or vice versa). Stripe generates separate secrets for each. Verify whsec_test_ vs. whsec_live_ prefixes match your environment.
2. Cloudflare/Proxy Modifying Request Body
Some reverse proxies or WAF rules may decompress or re-encode the webhook payload. Test by logging req.body.toString('hex') and comparing checksums with Stripe's Dashboard event logs.
3. Localhost Testing Without Proper Tunnel
Using curl to simulate webhooks bypasses Stripe's actual signing. Always use stripe listen --forward-to localhost:3000/webhook or a tool like ngrok/localtunnel with your real endpoint secret.
---
Verification Checklist
express.raw({type: 'application/json'}) on webhook route onlyendpointSecret starts with whsec_ (not sk_test_ or pk_test_)express.json() middleware *before* the webhook routestripe listen locally, not manual curl requests---
Quick Links
---
Found a different variation? Drop it in the comments—webhook issues often hide environment-specific quirks we can all learn from.