Stripe: webhook signature verification failing [2026 fix]
Webhook signature mismatch caused by wrong endpoint secret or missing raw body parsing—use correct secret from Dashboard and parse raw request body.
Stripe: Webhook Signature Verification Failing [2am Emergency Fix]
TL;DR
Cause: You're using the wrong webhook endpoint secret or parsing the request body as JSON before signature verification. Fix: Grab the correctwhsec_ secret from Stripe Dashboard → Developers → Webhooks, and verify the signature against the raw request body *before* parsing.---
Real Console Error Messages
``` Error: No signatures found matching the expected signature for payload. at WebhookEndpoint.constructEvent (index.js:142:15) ```
``` StripeSignatureVerificationError: No signatures found matching the expected signature for payload. Webhook secret provided was: 'whsec_test_***' ```
``` throw new errors.StripeSignatureVerificationError( ^ StripeSignatureVerificationError: Timestamp outside the tolerance zone ```
``` Error: Invalid signature. Expected sig=sha256=abc123..., got sig=sha256=xyz789... ```
``` Failed to verify webhook signature: expected body to be a string or Buffer ```
---
The Problem: Broken vs Fixed Code
❌ BROKEN: Parsing JSON Before Signature Check
```javascript // Express.js - WRONG ❌ app.post('/webhook', express.json(), (req, res) => { const sig = req.headers['stripe-signature']; // ERROR: Body was already parsed to JSON object const event = stripe.webhooks.constructEvent( req.body, // ← THIS IS AN OBJECT NOW, NOT RAW STRING sig, process.env.STRIPE_WEBHOOK_SECRET ); res.json({received: true}); }); ```
✅ FIXED: Raw Body + Signature Verification
```javascript
// Express.js - CORRECT ✅
app.post('/webhook',
express.raw({type: 'application/json'}), // ← Raw buffer, not parsed
(req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
// Pass raw body string/buffer first
event = stripe.webhooks.constructEvent(
req.body, // ← NOW A BUFFER - CORRECT
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.log(Webhook Error: ${err.message});
return res.status(400).send(Webhook Error: ${err.message});
}
// NOW safe to process event
if (event.type === 'payment_intent.succeeded') {
console.log('Payment succeeded!');
}
res.json({received: true});
}
);
```
❌ BROKEN: Wrong Secret
```javascript // Using pk_live_ (public key) instead of whsec_ (webhook secret) const secret = process.env.STRIPE_PUBLIC_KEY; // ← WRONG const event = stripe.webhooks.constructEvent(req.body, sig, secret); ```
✅ FIXED: Correct Secret
```javascript // Get from Stripe Dashboard → Developers → Webhooks → Signing secret const secret = process.env.STRIPE_WEBHOOK_SECRET; // ← whsec_xxx const event = stripe.webhooks.constructEvent(req.body, sig, secret); ```
---
Step-by-Step Verification Checklist
1. Confirm the Secret
whsec_test_ or whsec_live_ value.env: STRIPE_WEBHOOK_SECRET=whsec_xxx2. Verify Raw Body Handling
```javascript // Express app.post('/webhook', express.raw({type: 'application/json'}), ...)
// Fastify app.post('/webhook', async (request, reply) => { const sig = request.headers['stripe-signature']; const event = stripe.webhooks.constructEvent( request.rawBody, // ← Fastify exposes raw body here sig, process.env.STRIPE_WEBHOOK_SECRET ); })
// Next.js API Routes export const config = { api: { bodyParser: false, // ← Disable automatic parsing }, };
export default async function handler(req, res) { const buf = await buffer(req); const sig = req.headers['stripe-signature']; const event = stripe.webhooks.constructEvent( buf, // ← Pass the buffer sig, process.env.STRIPE_WEBHOOK_SECRET ); } ```
3. Check Environment Variable Loading
```bash
Verify secret is actually loaded
echo $STRIPE_WEBHOOK_SECRETShould output: whsec_test_51234567890...
```---
Still Broken? Check These Too
1. Timestamp Tolerance (too old) – Stripe rejects webhooks older than 5 minutes by default. Check server time is synchronized. Adjust tolerance: stripe.webhooks.constructEvent(body, sig, secret, 600) for 10-minute window.
2. Multiple Endpoints with Different Secrets – If you have test and live endpoints, ensure you're using the correct secret for the environment. Test webhooks use whsec_test_, production uses whsec_live_. [See webhook environment guide](/?guide=stripe-environments).
3. Proxy/Load Balancer Modifying Body – Intermediate servers sometimes compress or modify the raw request. Disable compression for /webhook routes or use a direct endpoint. [See reverse proxy troubleshooting](/?guide=webhook-proxy-issues).
---
Version Notes
Behavior unchanged in stripe@11.x through stripe@14.x (2024-2026). Raw body handling is consistent across frameworks, but framework setup differs—verify your framework's documentation for disabling body parsing.
---
Official Resources
---
Found a different variation? Drop it in the comments.