Stripe: webhook signature verification failing [2026 fix]
Webhook signature mismatch from endpoint secret or signing algorithm mismatch—validate secret against dashboard and use correct signing timestamp.
Stripe: Webhook Signature Verification Failing [2am Emergency Fix]
TL;DR
Cause: Your endpoint secret doesn't match the one Stripe is using, OR your code is signing the wrong request body/timestamp combination. Fix: Pull the EXACT secret from your Stripe dashboard (not from env file), ensure you're using the raw request body (not parsed JSON), and verify the timestamp parameter exists.---
Real Console Error Messages
``` [ERROR] Error: No matching signing secret found. spect signature c7d34a2f9c8b1e2d3f4a5b6c7d8e9f0a1b2c3d4e against endpoint signing secret.
[ERROR] SignatureVerificationError: Unable to verify request signature. Most likely cause: Using request body after it has been read or converted to JSON.
[ERROR] Webhook signature verification failed: timestamp outside the tolerance window. Current time: 1703088000, Webhook timestamp: 1703087500, Tolerance: 300s
[ERROR] webhook endpoint returned an error: 401 sig_verification_failed Request headers missing 'stripe-signature' or malformed format.
[ERROR] HMAC signature mismatch. Expected: v1=a1b2c3d4e5f6..., Received: v1=z9y8x7w6v5u4... ```
---
The Problem: Broken vs Fixed Code
❌ BROKEN CODE (Common Mistakes)
```javascript
// MISTAKE #1: Using parsed body instead of raw
app.post('/webhook', express.json(), (req, res) => {
const sig = req.headers['stripe-signature'];
const body = req.body; // ❌ WRONG: Already parsed!
try {
const event = stripe.webhooks.constructEvent(
body, // ❌ Should be RAW string, not object
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(Webhook Error: ${err.message});
}
});
// MISTAKE #2: Wrong secret source const secret = process.env.STRIPE_WEBHOOK_SECRET; // ❌ Stale env var
// MISTAKE #3: Stripping timestamp from signature header const sig = req.headers['stripe-signature'].split(',')[0]; // ❌ Breaks verification ```
✅ FIXED CODE (Production Ready)
```javascript
// FIX #1: Capture raw body BEFORE parsing
app.post(
'/webhook',
express.raw({type: 'application/json'}), // ✅ Keep body as Buffer
(req, res) => {
const sig = req.headers['stripe-signature'];
const body = req.body; // ✅ Raw buffer, not parsed
try {
const event = stripe.webhooks.constructEvent(
body, // ✅ Pass raw Buffer/string
sig,
process.env.STRIPE_WEBHOOK_SECRET // ✅ Verify this matches dashboard
);
// Handle specific events
switch (event.type) {
case 'payment_intent.succeeded':
// Process payment
break;
}
res.json({received: true});
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).send(Webhook Error: ${err.message});
}
}
);
// FIX #2: Verify secret against dashboard // Go to: Stripe Dashboard → Developers → Webhooks → Select endpoint // Copy the "Signing secret" (starts with 'whsec_') const WEBHOOK_SECRET = 'whsec_live_51A234567890abcdefghijk'; // ✅ From dashboard
// FIX #3: Keep full signature header intact const sig = req.headers['stripe-signature']; // ✅ Pass complete header ```
---
Step-by-Step Verification
1. Find your real secret:
- Log into [Stripe Dashboard](https://dashboard.stripe.com)
- Navigate: Developers → Webhooks
- Click your endpoint
- Copy "Signing secret" (whsec_*)
- Update your .env file
- Restart your server
2. Test the endpoint locally: ```bash # Use Stripe CLI to forward webhooks stripe listen --forward-to localhost:3000/webhook stripe trigger payment_intent.succeeded ``` If this works locally, your code is correct.
3. Verify request body handling: ```javascript // Debug: Log what you're receiving app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { console.log('Body type:', typeof req.body); console.log('Body is Buffer:', Buffer.isBuffer(req.body)); console.log('Signature:', req.headers['stripe-signature']); }); ```
---
Still Broken? Check These Too
1. Timestamp drift: Server clock skewed >5 minutes from actual time. Run date command; if off, sync with NTP.
2. Multiple webhook endpoints: You might be testing against a different endpoint. Verify the URL in dashboard matches your deployment.
3. Middleware ordering: If you have multiple express.json() calls, only the first one capturing raw body will work. Check middleware stack order in your Express setup. See [middleware guide](/guide=middleware-order) for details.
4. Stripe SDK version mismatch: We're uncertain if behavior changed in versions <2.0. Run npm list stripe and consult [Stripe Node.js docs](https://github.com/stripe/stripe-node) for your version.
5. Custom parsing: If you're using a body parser like Busboy or Multer before Stripe middleware, you've already consumed the body stream. See [request body](/guide=request-body-stream) guide.
---
Quick Checklist
whsec_ (not sk_)express.raw() not express.json()req.body as Buffer, not objectstripe trigger payment_intent.succeeded---
Official Docs
Found a different variation? Drop it in the comments.