Stripe: webhook signature verification failing [2026 fix]

Webhook signature mismatch due to raw body not preserved or wrong signing secret. Ensure raw request body is passed to signature verification, not parsed JSON.

Stripe: Webhook Signature Verification Failing [2026 Fix]

TL;DR

Cause: Your framework is parsing the request body before Stripe's signature verification runs, destroying the raw bytes needed for cryptographic validation. Fix: Pass the raw unparsed request body to stripe.Webhook.constructEvent() instead of parsed JSON.

---

Real Console Error Messages

``` Error: No signatures found matching the expected signature for payload. Managed signing secret mismatch. ```

``` StripeSignatureVerificationError: No signatures found matching the expected signature for payload. Possible reasons: (1) webhook secret is incorrect, (2) payload was not sent as received ```

``` Error: Invalid signature: sig_xxx does not match computed signature. Webhook endpoint received event but verification failed at timestamp validation. ```

``` TypeError: Cannot read property 'rawBody' of undefined at verifyWebhookSignature (/app/webhooks.js:42:15) ```

``` Stripe webhook error: Signature verification failed. Computed sig=sha256=abc... but received sig=sha256=xyz... Payload mismatch detected. ```

---

The Problem: Side-by-Side Code Fix

❌ BROKEN CODE (Express.js)

```javascript const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const app = express();

// WRONG: Body parser runs BEFORE webhook handler app.use(express.json());

app.post('/webhooks/stripe', (req, res) => { const sig = req.headers['stripe-signature']; try { // BROKEN: req.body is already parsed JSON, raw bytes lost const event = stripe.webhooks.constructEvent( req.body, // ← This is a JavaScript object, not raw bytes sig, process.env.STRIPE_WEBHOOK_SECRET ); res.json({ received: true }); } catch (err) { res.status(400).send(Webhook Error: ${err.message}); } }); ```

✅ FIXED CODE (Express.js)

```javascript const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const app = express();

// CORRECT: Raw body middleware runs FIRST, ONLY for Stripe webhooks app.post( '/webhooks/stripe', express.raw({ type: 'application/json' }), // ← Raw body, not parsed (req, res) => { const sig = req.headers['stripe-signature']; try { // FIXED: req.body is raw Buffer, signature verification works const event = stripe.webhooks.constructEvent( req.body, // ← This is Buffer with original bytes sig, process.env.STRIPE_WEBHOOK_SECRET ); // Handle event if (event.type === 'payment_intent.succeeded') { console.log('Payment succeeded:', event.data.object.id); } res.json({ received: true }); } catch (err) { console.error('Webhook signature verification failed:', err.message); res.status(400).send(Webhook Error: ${err.message}); } } );

// Parse JSON for OTHER routes app.use(express.json()); ```

❌ BROKEN CODE (Next.js API Route)

```javascript // pages/api/webhooks/stripe.js - BROKEN export default async (req, res) => { const sig = req.headers['stripe-signature']; const body = JSON.stringify(req.body); // ← Re-stringifying loses original try { const event = stripe.webhooks.constructEvent( body, sig, process.env.STRIPE_WEBHOOK_SECRET ); } catch (err) { return res.status(400).json({ error: err.message }); } }; ```

✅ FIXED CODE (Next.js API Route)

```javascript // pages/api/webhooks/stripe.js - FIXED export const config = { api: { bodyParser: false, // ← Disable automatic body parsing }, };

export default async (req, res) => { const sig = req.headers['stripe-signature']; // Read raw body as Buffer const chunks = []; for await (const chunk of req) { chunks.push(chunk); } const rawBody = Buffer.concat(chunks); try { const event = stripe.webhooks.constructEvent( rawBody, // ← Original bytes preserved sig, process.env.STRIPE_WEBHOOK_SECRET ); res.status(200).json({ received: true }); } catch (err) { res.status(400).json({ error: err.message }); } }; ```

---

Why This Happens

Stripe signs the exact raw request body bytes. When your framework automatically parses JSON:

1. Original bytes → parsed JavaScript object 2. Object → re-serialized (different whitespace/order) 3. Signature hash = different result 4. Verification fails

The cryptographic signature is only valid for the exact bytes Stripe sent. Any transformation breaks it.

---

Still Broken? Check These Too

1. Wrong webhook secret: Verify STRIPE_WEBHOOK_SECRET in .env matches Stripe Dashboard → Developers → Webhooks. Copy the entire whsec_xxx string, not your signing secret.

2. Multiple body parsers: If using app.use(express.json()) globally, it runs before route handlers. Move it AFTER webhook routes or create a separate app instance. See [debugging middleware order](/?guide=express-middleware-order) for details.

3. Running old Stripe SDK: stripe npm package < v8 has different API. Verify version: npm list stripe. Update with npm install stripe@latest. Version behavior differs; we're testing against v14+ but cannot guarantee older versions.

---

Testing Your Fix

```bash

Use Stripe CLI to send test event

stripe listen --forward-to localhost:3000/webhooks/stripe stripe trigger payment_intent.succeeded ```

You should see: Webhook signature verification failed: No signatures found... → disappeared after applying fix.

---

Related Resources

  • [Stripe webhook security best practices](/?guide=stripe-webhook-security)
  • [Express.js body parsing gotchas](/?guide=express-body-parser-issues)
  • Official: [Stripe Webhook Signature Verification Docs](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