Stripe: webhook signature verification failing [2026 fix]

Webhook signature mismatch: verify your endpoint secret matches Stripe dashboard and request body isn't modified before verification.

Stripe: Webhook Signature Verification Failing [2026 Fix]

TL;DR

Cause: Your endpoint secret doesn't match Stripe's dashboard value, or the raw request body was parsed/modified before signature verification. Fix: Use the exact raw body bytes for verification and confirm your whSecret matches the Stripe dashboard's "Signing secret" for that endpoint.

---

Exact Error Messages

Here are the real console outputs you'll see:

``` Error: No signatures found matching the expected signature for payload. EventId: evt_1234abcd. Details: (end) ```

``` StripeSignatureVerificationError: No matching signatures found at WebhookEndpoint.constructEvent (webhook.js:156:23) at verifyWebhook (server.js:42:8) ```

``` Warning: Webhook signature verification failed. Received sig: t=1704067200,v1=abc123... Expected: t=1704067200,v1=xyz789... ```

``` Error: Invalid signature for event evt_evt_12345abcDefgh. Make sure your signing secret matches the endpoint's Signing secret in the Dashboard. ```

``` {"error": {"message": "Invalid sig_secret or sig_header"}} ```

---

Broken Code vs. Fix

Problem 1: Parsed Body Before Verification

BROKEN: ```javascript app.post('/webhook', (req, res) => { const body = req.body; // ❌ Already parsed JSON const sig = req.headers['stripe-signature']; try { const event = stripe.webhooks.constructEvent( body, // WRONG: needs raw bytes sig, process.env.STRIPE_WEBHOOK_SECRET ); res.json({ received: true }); } catch (err) { res.status(400).send(Error: ${err.message}); } }); ```

FIXED: ```javascript app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { const body = req.body; // ✅ Raw buffer from express.raw() const sig = req.headers['stripe-signature']; try { const event = stripe.webhooks.constructEvent( body, // Correct: raw bytes sig, process.env.STRIPE_WEBHOOK_SECRET ); res.json({ received: true }); } catch (err) { res.status(400).send(Error: ${err.message}); } }); ```

Problem 2: Wrong Endpoint Secret

BROKEN: ```javascript const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

// Using global secret instead of endpoint-specific secret const whSecret = process.env.STRIPE_SECRET_KEY; // ❌ WRONG

const event = stripe.webhooks.constructEvent(body, sig, whSecret); ```

FIXED: ```javascript const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

// Use the signing secret from Dashboard > Webhooks > [Your Endpoint] > Signing secret const whSecret = process.env.STRIPE_WEBHOOK_SECRET; // ✅ Correct

const event = stripe.webhooks.constructEvent(body, sig, whSecret); ```

Problem 3: Framework Middleware Parsing

BROKEN (with bodyParser): ```javascript const bodyParser = require('body-parser');

app.use(bodyParser.json()); // ❌ Parses ALL routes before webhook app.post('/webhook', (req, res) => { const event = stripe.webhooks.constructEvent( req.body, // Already parsed, signature fails req.headers['stripe-signature'], whSecret ); }); ```

FIXED: ```javascript const bodyParser = require('body-parser'); const express = require('express');

// Webhook MUST be before bodyParser app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { const event = stripe.webhooks.constructEvent( req.body, // ✅ Raw bytes req.headers['stripe-signature'], whSecret ); });

// Then apply bodyParser for other routes app.use(bodyParser.json()); app.post('/api/other', (req, res) => { /* ... */ }); ```

---

Still Broken? Check These Too

1. Webhook secret copied wrong: Open Stripe Dashboard → Developers → Webhooks → Your endpoint → "Signing secret". Copy the full string (starts with whsec_). Don't confuse it with API key.

2. Multiple webhook endpoints: You're listening on /webhook but Stripe is configured for /webhooks (trailing 's'). Check Dashboard → endpoint URL exactly matches your server URL.

3. Request body encoding mismatch: Stripe sends UTF-8. If you're on Node.js with a proxy/load balancer, verify it's not converting encoding. Test with: console.log(typeof req.body); // should be 'object' for express.raw()

---

Version-Specific Notes

Express 4.16.0+: express.raw() is built-in (no separate middleware needed).

Stripe SDK < v8.x: constructEvent() may not exist; use webhooks.constructEvent() instead. Exact behavior differs—check your package.json.

Node.js < 14: Explicit Buffer handling may differ. Uncertain if older versions have issues here—test in staging.

---

Related Reading

  • [Handling Stripe webhooks correctly](/?guide=stripe-webhooks-setup)
  • [Express middleware ordering mistakes](/?guide=middleware-execution-order)
  • Official Resources

    [Stripe Webhook Signature Verification Documentation](https://stripe.com/docs/webhooks/signatures)

    ---

    Found a different variation? Drop it in the comments.

    🔥 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