Stripe: webhook signature verification failing [2026 fix]

Webhook signature mismatch from endpoint secret misconfiguration or timestamp validation. Use correct signing secret and verify within 5-minute window.

Stripe: Webhook Signature Verification Failing [2026 Fix]

TL;DR

Cause: Your endpoint is using the wrong signing secret (often the API key instead of the webhook endpoint secret) or the request timestamp is outside the 5-minute verification window.

Fix: Replace your secret with the correct whsec_* or we_* endpoint secret from Stripe Dashboard → Developers → Webhooks, and ensure server time is synchronized.

---

Exact Error Messages You'll See

These are real console outputs from webhook verification failures:

``` Error: No signatures found matching the expected signature for payload. ```

``` Stripe.error.SignatureVerificationError: Timestamp outside the tolerance window ```

``` Webhook signature verification failed: Invalid signature ```

``` Error: Unable to extract timestamp and signatures from header 'stripe-signature' ```

``` Fatal error in Stripe webhook handler: Signature verification failed for event evt_* ```

---

The Problem: Broken vs. Fixed Code

❌ BROKEN CODE (Common Mistakes)

```javascript // Mistake 1: Using API key instead of webhook secret const stripe = require('stripe')(process.env.STRIPE_API_KEY); const secret = process.env.STRIPE_API_KEY; // WRONG!

app.post('/webhook', (req, res) => { try { const event = stripe.webhooks.constructEvent( req.rawBody, req.headers['stripe-signature'], secret // This will ALWAYS fail ); } catch (err) { return res.status(400).send(Webhook Error: ${err.message}); } }); ```

```javascript // Mistake 2: Not using raw body buffer app.use(express.json()); // Parsed JSON breaks signature

app.post('/webhook', (req, res) => { const event = stripe.webhooks.constructEvent( req.body, // Already parsed - signature won't match! req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET ); }); ```

✅ FIXED CODE

```javascript // Correct implementation const stripe = require('stripe')(process.env.STRIPE_API_KEY); const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; // whsec_live_* or whsec_test_*

// CRITICAL: Handle webhook endpoint BEFORE express.json() app.post( '/webhook', express.raw({type: 'application/json'}), // Raw buffer, not parsed (req, res) => { const sig = req.headers['stripe-signature']; let event; try { event = stripe.webhooks.constructEvent( req.body, // Raw buffer from express.raw() sig, webhookSecret // MUST be whsec_* endpoint secret ); } catch (err) { console.error(Webhook signature verification failed: ${err.message}); return res.status(400).send(Webhook Error: ${err.message}); } // Handle event switch (event.type) { case 'payment_intent.succeeded': // Process payment break; case 'charge.failed': // Handle failure break; } res.json({received: true}); } );

// Other routes CAN use express.json() app.use(express.json()); app.post('/api/other', (req, res) => { // This is fine }); ```

Key differences: 1. express.raw({type: 'application/json'}) middleware preserves the raw request body 2. webhookSecret starts with whsec_ (not your API key) 3. Webhook endpoint is defined BEFORE express.json()

---

Verification Checklist

1. Get the right secret: - Go to [Stripe Dashboard](https://dashboard.stripe.com/webhooks) → Developers → Webhooks - Find your endpoint URL - Click "Reveal" next to "Signing secret" - Copy the full whsec_* value

2. Verify environment variable: ```bash echo $STRIPE_WEBHOOK_SECRET # Should output: whsec_live_ABC123... or whsec_test_ABC123... # NOT your API key (sk_live_* or sk_test_*) ```

3. Check server time synchronization: ```javascript // Stripe rejects signatures older than 5 minutes console.log(new Date().toISOString()); // Should be within ±5 minutes of actual time ```

4. Test with Stripe CLI locally: ```bash stripe listen --forward-to localhost:3000/webhook stripe trigger payment_intent.succeeded ```

---

Still broken? Check these too

1. Multiple webhooks registered: You might be testing against the wrong endpoint. Verify in Dashboard that the URL exactly matches your live server (no staging/dev URLs mixed in).

2. Raw body middleware conflict: If using third-party middleware that modifies the request body before your webhook handler, it will break the signature. Load webhook route before any body-parsing middleware [related](/?guide=express-middleware-order).

3. Stripe library version: We're certain about behavior in stripe@14.0.0+ (2024+). If you're on stripe@8.x or earlier, timestamp tolerance may differ—check official docs for your version [related](/?guide=stripe-version-compatibility).

---

References

  • Official Stripe Webhook Docs: https://stripe.com/docs/webhooks/signatures
  • Node.js SDK Reference: https://github.com/stripe/stripe-node
  • Found a different variation? Drop it in the comments—we update this guide based on real production issues.

    🔥 0d
    LIVE
    ↓ PlanetScale rage spiking •⚠ Vercel pricing complaints •↑ Railway gaining fast •↑ Supabase happiness rising •↑ Resend loved by devs •↓ PlanetScale rage spiking •⚠ Vercel pricing complaints •↑ Railway gaining fast •↑ Supabase happiness rising
    DEVELOPER PAIN RADAR // Loading...

    Developers complain.
    Opportunities appear.

    We track what developers are struggling with today — and what opportunities that creates.

    —
    guides today
    —
    avg happiness
    🔥 Pain
    📖 Guides
    🔭 Explore
    👤 Mine
    🔥 Pain Radar — rage scores today
    ↗ share
    💡 Opportunity Feed — pain = market gap
    📈 Tool Momentum
    all scores →
    📖 Latest Guide
    all guides →
    📖 All Guides
    📊 Tool Scores
    + Submit
    📰 Hacker News
    ➕ Submit a Tool
    ← back