Stripe: webhook signature verification failing [2026 fix]
Your webhook secret is mismatched or endpoint isn't receiving raw request body. Move to Stripe CLI or verify signing secret matches webhook endpoint config.
Stripe: Webhook Signature Verification Failing [2026 Fix]
TL;DR
Cause: You're signing the request with one secret but Stripe is sending webhooks signed with a different secret, OR your framework is parsing JSON before signature verification runs.
Fix: Use the *endpoint secret* (not API key) from your Stripe Dashboard webhook settings, and verify signatures BEFORE parsing the request body.
---
Real Console Error Messages
Here are exact errors you'll see at 2am:
``` Error: No signatures found matching the expected signature for payload. Webhook secret (signing secret) may be incorrect. ```
``` StripeSignatureVerificationError: Timestamp outside the tolerance zone at verifySignatureWithMethod (/node_modules/stripe/lib/utils.js:156:23) ```
``` Error: Invalid signature. Expected sig=[hash], but request signature was sig=[different_hash] ```
``` Webhook Error: Unable to verify webhook signature for endpoint /webhook/stripe Signature: t=1704067200,v1=abc123... Secret mismatch detected ```
``` TypeError: Cannot read property 'id' of undefined at verifySignature (stripe.js:45:12) ```
---
The Problem: Broken vs Fixed Code
❌ BROKEN (Most Common)
```javascript
// Express.js endpoint
app.post('/webhook/stripe', express.json(), (req, res) => {
const sig = req.headers['stripe-signature'];
const secret = process.env.STRIPE_API_KEY; // ← WRONG! Use webhook secret
try {
const event = stripe.webhooks.constructEvent(
JSON.stringify(req.body), // ← WRONG! Body already parsed by middleware
sig,
secret
);
res.json({received: true});
} catch (err) {
console.log('Webhook error:', err.message);
res.status(400).send(Webhook Error: ${err.message});
}
});
```
Three problems here:
1. Using STRIPE_API_KEY instead of STRIPE_WEBHOOK_SECRET
2. express.json() middleware parsed the body before verification
3. Passing stringified already-parsed body breaks signature verification
✅ FIXED
```javascript
// Express.js endpoint - CORRECT
app.post(
'/webhook/stripe',
express.raw({type: 'application/json'}), // ← Raw buffer, NOT parsed
(req, res) => {
const sig = req.headers['stripe-signature'];
const secret = process.env.STRIPE_WEBHOOK_SECRET; // ← Correct secret
try {
const event = stripe.webhooks.constructEvent(
req.body, // ← Raw buffer from express.raw()
sig,
secret
);
// Handle events
switch (event.type) {
case 'payment_intent.succeeded':
console.log(Payment succeeded: ${event.data.object.id});
break;
case 'charge.refunded':
console.log(Charge refunded: ${event.data.object.id});
break;
}
res.json({received: true});
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).send(Webhook Error: ${err.message});
}
}
);
```
What changed:
express.json() → express.raw({type: 'application/json'}) keeps body as raw BufferSTRIPE_API_KEY → STRIPE_WEBHOOK_SECRET (find in Dashboard > Developers > Webhooks)For Next.js / Vercel
```javascript export const config = { api: { bodyParser: false, // ← Critical: disable auto-parsing }, };
export default async function handler(req, res) {
if (req.method !== 'POST') return res.status(405).end();
const sig = req.headers['stripe-signature'];
const secret = process.env.STRIPE_WEBHOOK_SECRET;
// Read raw body
const rawBody = await buffer(req);
try {
const event = stripe.webhooks.constructEvent(rawBody, sig, secret);
res.status(200).json({received: true});
} catch (err) {
return res.status(400).send(Webhook Error: ${err.message});
}
}
async function buffer(readable) { const chunks = []; for await (const chunk of readable) { chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk); } return Buffer.concat(chunks); } ```
---
Still Broken? Check These Too
1. Timestamp tolerance exceeded — Stripe rejects signatures older than 5 minutes. Check your server clock is synchronized (run ntpdate -s time.nist.gov on Unix). Webhook was sent at t=1704067200 but your server timestamp is drifted.
2. Wrong environment — You added the webhook URL to your *test* Stripe account but your code uses STRIPE_WEBHOOK_SECRET from *production*. Toggle between test/live mode in Dashboard. Use separate environment variables: STRIPE_WEBHOOK_SECRET_TEST vs STRIPE_WEBHOOK_SECRET_LIVE.
3. Proxy or firewall stripping headers — CloudFlare, nginx, or API Gateway might strip stripe-signature headers. Verify the header reaches your endpoint by logging: console.log('Headers:', req.headers) before verification. If missing, whitelist the header in your proxy config.
---
Quick Verification Checklist
whsec_test_ or whsec_live_sk_test_ or sk_live_)stripe-signature header is present in logsstripe listen --forward-to localhost:3000/webhook/stripe---
Related Guides
---
Official Reference
[Stripe Webhook Signature Verification Docs](https://stripe.com/docs/webhooks/signatures)
---
Found a different variation? Drop it in the comments.
Stripe updates their client libraries regularly. If your error message doesn't match these but involves webhook signatures, describe: (1) your framework version, (2) your exact error output, and (3) your signing code. We'll add it to this guide.