Stripe: webhook signature verification failing [2026 fix]
Webhook signature mismatch usually means misaligned endpoint secret or raw body modification. Verify you're using the correct signing secret and passing the raw request body untouched.
Stripe: Webhook Signature Verification Failing [2026 Fix]
TL;DR
Cause: Your endpoint is using the wrong webhook signing secret or receiving a parsed/modified request body instead of raw bytes. Fix: Match your endpoint secret to Stripe Dashboard, ensure middleware doesn't parse the body before signature verification, and always usereq.rawBody or equivalent raw stream.---
Real Console Error Messages
``` Error: No signatures found matching the expected signature for payload. ```
``` StripeSignatureVerificationError: Unable to extract timestamp and signatures from header 'stripe-signature' ```
``` Webhook signature verification failed: Computed signature does not match stripe-signature header ```
``` Error: Webhook endpoint returned 500 Internal Server Error - Webhook endpoint failed. Reason: Webhook signature verification failed ```
``` StripeSignatureVerificationError: Invalid signature: timestamp outside the tolerance window ```
---
The Problem: Code Comparison
❌ BROKEN CODE
```javascript // Express.js - WRONG const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.use(express.json()); // ⚠️ PROBLEM: parses body BEFORE webhook handler
app.post('/webhook', (req, res) => {
const sig = req.headers['stripe-signature'];
const secret = process.env.STRIPE_API_KEY; // ⚠️ WRONG: using secret key instead of webhook secret
try {
const event = stripe.webhooks.constructEvent(req.body, sig, secret);
// req.body is already parsed - signature verification fails
res.json({received: true});
} catch (err) {
res.status(400).send(Webhook Error: ${err.message});
}
});
```
✅ CORRECT CODE
```javascript // Express.js - CORRECT const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// ✅ FIX: Keep webhook route BEFORE express.json() middleware
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
const secret = process.env.STRIPE_WEBHOOK_SIGNING_SECRET; // ✅ Use webhook signing secret
try {
// ✅ req.body is raw Buffer, signature verification works
const event = stripe.webhooks.constructEvent(req.body, sig, secret);
switch(event.type) {
case 'payment_intent.succeeded':
console.log('Payment succeeded:', event.data.object.id);
break;
case 'charge.failed':
console.log('Charge failed:', event.data.object.id);
break;
}
res.json({received: true});
} catch (err) {
console.error('Webhook error:', err.message);
res.status(400).send(Webhook Error: ${err.message});
}
});
// ✅ FIX: Add other routes AFTER webhook endpoint app.use(express.json()); app.post('/api/other-route', (req, res) => { // This route gets parsed JSON safely }); ```
Alternative Fix for Next.js API Routes
```javascript // pages/api/webhooks/stripe.js - CORRECT import { buffer } from 'micro'; import stripe from 'stripe';
const stripeClient = stripe(process.env.STRIPE_SECRET_KEY); const webhookSecret = process.env.STRIPE_WEBHOOK_SIGNING_SECRET;
export const config = { api: { bodyParser: false, // ✅ Disable automatic body parsing }, };
export default async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).end(); }
try { const buf = await buffer(req); // ✅ Get raw buffer const sig = req.headers['stripe-signature']; const event = stripeClient.webhooks.constructEvent( buf, sig, webhookSecret );
// Handle event
console.log(Event type: ${event.type});
return res.json({received: true});
} catch (err) {
return res.status(400).send(Webhook error: ${err.message});
}
}
```
---
Critical Setup Checklist
1. Webhook Signing Secret: Go to [Stripe Dashboard](https://dashboard.stripe.com/webhooks) → Click your endpoint → Copy signing secret (starts with whsec_)
✅ Verify it matches process.env.STRIPE_WEBHOOK_SIGNING_SECRET exactly
2. Middleware Order:
✅ Webhook route MUST come BEFORE express.json() middleware
3. Raw Body Requirement: ✅ Signature verification needs the exact bytes Stripe sent—no parsing allowed
4. Timestamp Tolerance: ✅ Default window is 300 seconds; if your server time is severely out of sync, sync system clock
---
Still Broken? Check These Too
1. Using STRIPE_API_KEY instead of webhook secret: The signing secret is different from your API key. They both exist in Stripe Dashboard but serve different purposes. [View webhook authentication guide](/?guide=stripe-auth)
2. Body already parsed by middleware: If you're using body-parser, morgan with logging, or other middleware before the webhook route, the raw body is lost. Move the webhook handler before middleware chains.
3. Local testing with old timestamp: Stripe rejects webhooks older than 5 minutes. When replaying test events or testing with curl, ensure your system clock is synchronized. Use date command to verify. [Learn about webhook testing locally](/?guide=stripe-local-testing)
---
Version Note
This guide covers Stripe Node.js SDK v13+. If you're on v12 or earlier, stripe.webhooks.constructEvent() behavior differs slightly—verify your specific version's [official documentation](https://stripe.com/docs/webhooks/signatures#verify-manually).
---
Found a different variation? Drop it in the comments
Stripe webhook failures vary by framework, hosting environment, and middleware configuration. If you've hit a unique version—particularly with Python, PHP, or Go implementations—please share your error and fix below so others find it faster.