Stripe: webhook signature verification failing [2026 fix]
Webhook signature mismatch: endpoint secret misconfiguration or timestamp validation failure. Verify correct secret in environment variables and check system clock skew.
Stripe: Webhook Signature Verification Failing [2026 Fix]
TL;DR
Cause: You're using the wrong endpoint secret (publishable key instead of signing secret) OR your server's system clock is >5 minutes out of sync. Fix: ReplaceSTRIPE_WEBHOOK_SECRET with the signing secret from Stripe Dashboard > Developers > Webhooks, and ensure NTP is synced on your server.---
Exact Error Messages
You'll likely see one of these in your production logs at 2am:
``` Error: No signatures found matching the expected signature for payload. Webhook id: evt_xxx ```
``` StripeSignatureVerificationError: Unable to extract timestamp and signatures from header ```
``` Error: Timestamp outside the tolerance window ```
``` SignatureVerificationError: No valid signature headers detected ```
``` Error validating request signature: Invalid signature ```
---
The Problem: Side-by-Side Code
❌ BROKEN CODE
```javascript // Express webhook endpoint - INCORRECT const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
// WRONG: Using publishable key or incomplete secret
const sig = req.headers['stripe-signature'];
try {
const event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_API_KEY // ❌ WRONG: This is the API key, not webhook secret
);
res.json({received: true});
} catch (err) {
return res.status(400).send(Webhook Error: ${err.message});
}
});
```
✅ FIXED CODE
```javascript // Express webhook endpoint - CORRECT const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
try {
const event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SIGNING_SECRET // ✅ CORRECT: Use signing secret
);
// Handle specific events
switch(event.type) {
case 'payment_intent.succeeded':
console.log('Payment succeeded');
break;
case 'charge.failed':
console.log('Charge failed');
break;
}
res.json({received: true});
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).send(Webhook Error: ${err.message});
}
});
```
Key Differences
1. Secret source: Use STRIPE_WEBHOOK_SIGNING_SECRET (starts with whsec_), NOT STRIPE_SECRET_KEY (starts with sk_)
2. Environment variable naming: Be explicit and consistent
3. Error logging: Always log the raw error for debugging
---
How to Get the Right Secret
1. Go to [Stripe Dashboard](https://dashboard.stripe.com/)
2. Navigate to Developers → Webhooks
3. Click on your endpoint URL
4. Scroll to Signing secret section
5. Click Reveal to show the secret (format: whsec_live_xxxxx)
6. Copy and paste into your .env file:
``` STRIPE_WEBHOOK_SIGNING_SECRET=whsec_live_xxxxx ```
Critical: This is different for:
---
Clock Skew Issue
Stripe rejects signatures with timestamps older than 5 minutes. If your server clock is incorrect:
```bash
On Linux/Mac - check system time
dateSync with NTP
sudo ntpdate -s time.nist.govOr use timedatectl
sudo timedatectl set-ntp true ```If you're on AWS/Docker/Kubernetes, ensure:
amazon-cloudwatch-agent configuredTZ environment variable/etc/localtime---
Still Broken? Check These Too
1. [Raw body requirement](/guide=stripe-raw-body): Stripe signature verification requires the raw HTTP body, not parsed JSON. Ensure express.raw() middleware is applied *before* JSON parsing for this route only.
2. Endpoint URL mismatch: The webhook endpoint URL registered in Stripe Dashboard must exactly match your actual endpoint. Verify in Dashboard → Developers → Webhooks that the URL matches your server's path (including protocol: https://, not http://).
3. [Timeout configuration](/guide=stripe-webhook-timeout): Stripe expects a response within 30 seconds and retries failed webhooks. Long database operations inside the webhook handler cause timeouts—move heavy work to background jobs using Redis/Bull or AWS Lambda.
---
Version-Specific Notes
I'm uncertain about exact behavior differences in stripe-node versions before 8.0.0. If you're using an older version (check package.json), you may need to pass additional options to constructEvent(). Consult the [official Stripe Node.js library changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md).
---
Official Resources
---
Found a different variation? Drop it in the comments.