Stripe: webhook signature verification failing [2026 fix]
Webhook signature mismatch: your endpoint secret doesn't match Stripe's signing key. Use raw request body, not parsed JSON.
Stripe: Webhook Signature Verification Failing [2am Emergency Fix]
TL;DR
Cause: You're verifying the signature against parsed JSON instead of the raw request body. Fix: Passreq.rawBody or req.body (as buffer) to stripe.webhooks.constructEvent(), never the stringified version.---
Real Console Error Messages
``` Error: No signatures found matching the expected signature for payload. ```
``` StripeSignatureVerificationError: Unable to verify signature at constructEvent (/node_modules/stripe/lib/resources/WebhookEndpoints.js:123:45) ```
``` Webhook signature verification failed: {"error_code":"sig_verification_failed","message":"signature mismatch"} ```
``` Invalid signature. Expected signature: t=1704067200,v1=abc123..., got: t=1704067200,v1=def456... ```
``` Payload verification failed. Raw body required for HMAC-SHA256 validation but received stringified JSON. ```
---
The Problem: Broken vs. Fixed Code
❌ BROKEN (Most Common Issue)
```javascript const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/webhook', express.json(), (req, res) => { const sig = req.headers['stripe-signature']; const endpointSecret = process.env.STRIPE_ENDPOINT_SECRET;
try {
// ❌ WRONG: Using req.body (parsed JSON object)
const event = stripe.webhooks.constructEvent(
req.body, // This is already parsed! Breaks HMAC verification
sig,
endpointSecret
);
console.log('Webhook verified');
res.json({ received: true });
} catch (err) {
console.error('Webhook error:', err.message);
res.status(400).send(Webhook Error: ${err.message});
}
});
```
✅ FIXED (Node.js/Express)
```javascript const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// ✅ CRITICAL: Use raw body middleware ONLY for webhook route app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { const sig = req.headers['stripe-signature']; const endpointSecret = process.env.STRIPE_ENDPOINT_SECRET;
try {
// ✅ CORRECT: req.body is a Buffer when using express.raw()
const event = stripe.webhooks.constructEvent(
req.body, // Raw buffer—HMAC signature verified correctly
sig,
endpointSecret
);
// Handle specific events
if (event.type === 'payment_intent.succeeded') {
console.log('✅ Payment successful:', event.data.object.id);
}
res.json({ received: true });
} catch (err) {
console.error('❌ Webhook error:', err.message);
res.status(400).send(Webhook Error: ${err.message});
}
});
// ✅ Other routes use JSON parsing normally app.post('/api/other', express.json(), (req, res) => { // req.body is parsed JSON here res.json({ ok: true }); }); ```
✅ FIXED (Next.js API Routes)
```javascript // pages/api/webhooks/stripe.js import { buffer } from 'micro'; const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
export const config = { api: { bodyParser: false, // ✅ Disable automatic JSON parsing }, };
export default async function handler(req, res) { if (req.method !== 'POST') return res.status(405).end();
const buf = await buffer(req); // ✅ Get raw buffer const sig = req.headers['stripe-signature']; const endpointSecret = process.env.STRIPE_ENDPOINT_SECRET;
try { const event = stripe.webhooks.constructEvent( buf, // ✅ Raw buffer, not JSON sig, endpointSecret ); res.status(200).json({ received: true }); } catch (err) { console.error('Webhook error:', err.message); res.status(400).end(); } } ```
---
Critical Setup Checklist
1. Endpoint Secret: Use the value from Stripe Dashboard → Developers → Webhooks (starts with whsec_), NOT your API key
2. Raw Body Required: The webhook route MUST receive raw bytes, not parsed JSON
3. Environment Variable: Confirm STRIPE_ENDPOINT_SECRET is loaded: console.log(process.env.STRIPE_ENDPOINT_SECRET)
4. Test with CLI: stripe listen --forward-to localhost:3000/webhook then trigger a test event in Dashboard
5. Version Note: Behavior is consistent in stripe@11.0.0+ (Dec 2023+), but older versions may have different signatures
---
Still Broken? Check These Too
1. [Webhook Event Type Filtering](/?guide=stripe-event-routing) — Verify you're subscribing to correct events in Stripe Dashboard. Some integrations miss charge.succeeded vs payment_intent.succeeded.
2. [Clock Skew / Timestamp Validation](/?guide=stripe-timestamp-tolerance) — Stripe rejects signatures if your server clock is >5 minutes off. Run ntpdate -s time.nist.gov (Linux) or sync time on Windows.
3. [Multiple Endpoint Secrets](/?guide=stripe-test-live-webhooks) — Accidentally using Live secret on Test webhook route (or vice versa). Dashboard shows separate secrets; confirm which one you're using.
---
Official Docs
[Stripe Webhook Signature Verification](https://stripe.com/docs/webhooks/signatures)---
Found a different variation? Drop it in the comments—common scenarios include Firebase Cloud Functions, AWS Lambda, or custom frameworks we haven't covered.