Stripe: webhook signature verification failing [2026 fix]
Webhook signature fails when endpoint secret is wrong or request body altered. Fix: use raw body buffer, not parsed JSON.
Stripe: Webhook Signature Verification Failing [2am Emergency Fix]
TL;DR
Cause: You're verifying signatures against parsed/stringified JSON instead of the raw request body bytes. Fix: Passreq.rawBody (Buffer) to stripe.webhooks.constructEvent(), never JSON.stringify(req.body).---
Real Console Error Messages
``` Error: No signatures found matching the expected signature for payload. at constructEvent (node_modules/stripe/lib/webhooks.js:67:23) ```
``` WebhookSignatureVerificationError: Unable to extract timestamp and signatures from header 'stripe-signature' ```
``` Error: Timestamp outside the tolerance window. ```
``` Signature mismatch: expected sig_1a2b3c, received sig_xyz789 ```
``` TypeError: Cannot read property 'toString' of undefined at verifySignature ```
---
Broken Code vs. Exact Fix
❌ BROKEN: Parsing Body Before Verification
```javascript
// Express endpoint - WRONG
app.post('/webhook', express.json(), (req, res) => {
const sig = req.headers['stripe-signature'];
try {
// ❌ WRONG: req.body is already parsed object
const event = stripe.webhooks.constructEvent(
JSON.stringify(req.body), // ❌ Signature mismatch!
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
res.json({ received: true });
} catch (err) {
res.status(400).send(Webhook Error: ${err.message});
}
});
```
✅ FIXED: Raw Body Buffer
```javascript
// Express endpoint - CORRECT
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['stripe-signature'];
try {
// ✅ CORRECT: req.body is Buffer, passed directly
const event = stripe.webhooks.constructEvent(
req.body, // ✅ Raw bytes - signature verified!
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
switch (event.type) {
case 'payment_intent.succeeded':
console.log('Payment succeeded:', event.data.object.id);
break;
case 'charge.failed':
console.log('Payment failed:', event.data.object.id);
break;
}
res.json({ received: true });
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
res.status(400).send(Webhook Error: ${err.message});
}
});
```
Why This Happens
Stripe signs the raw request body bytes. When Express parses JSON with express.json(), it converts the buffer to an object. If you then stringify it, the whitespace, key ordering, and encoding differ from the original—causing signature verification to fail.
Key difference:
express.json() → req.body = Object ❌express.raw() → req.body = Buffer ✅---
Step-by-Step Fix
Step 1: Verify Endpoint Secret
```bash
Check your Stripe Dashboard webhook endpoint
Settings → Webhooks → Click endpoint → Copy "Signing secret"
echo $STRIPE_WEBHOOK_SECRET # Should output: whsec_xxx... ```Step 2: Update Middleware
Change ONE line in your webhook route:
```javascript // FROM THIS: app.post('/webhook', express.json(), (req, res) => {
// TO THIS: app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { ```
Step 3: Verify constructEvent Call
Ensure you're passing req.body directly (it's now a Buffer):
```javascript const event = stripe.webhooks.constructEvent( req.body, // ✅ Buffer, not string sig, // ✅ From header process.env.STRIPE_WEBHOOK_SECRET // ✅ From env ); ```
Step 4: Restart and Test
```bash
Restart your server
npm startTest with Stripe CLI (if you have local setup)
stripe listen --forward-to localhost:3000/webhook stripe trigger payment_intent.succeeded ```---
Still Broken? Check These Too
1. Environment Variable Not Set
Confirm STRIPE_WEBHOOK_SECRET exists in your .env and process.env. Check Dashboard → Settings → Webhooks → Signing secret (starts with whsec_). Never use the API key by mistake.
2. Multiple Webhook Routes Conflict
If you have /webhook and /webhooks, ensure only ONE is registered in Stripe Dashboard. Multiple routes with different middleware can cause signature mismatches. [See route debugging guide](/?guide=express-routing).
3. Webhook Secret Rotated
Stripe Dashboard allows rotating signing secrets. If you regenerated it without updating .env, old signed requests will fail. Compare the whsec_ value in your code to the Dashboard.
4. Proxy or Middleware Altering Body If using body-parser, compression middleware, or a proxy (CloudFlare, nginx), verify they're not modifying the raw request. Stripe signature depends on byte-for-byte matching.
5. Timestamp Too Old
Stripe rejects signatures older than 5 minutes by default. If your server clock is drifting, sync NTP: ntpdate -s time.nist.gov (Linux) or check macOS System Preferences → Date & Time.
---
Security Note
Always validate stripe-signature header before processing. Never trust webhook data without verification—this prevents replay attacks and spoofing.
---
Resources
---
Found a different variation? Drop it in the comments.