Stripe: webhook signature verification failing [2026 fix]

Webhook signature fails when endpoint secret is wrong or request body altered. Fix: use raw body buffer, not parsed JSON.

Stripe: Webhook Signature Verification Failing [2am Emergency Fix]

TL;DR

Cause: You're verifying signatures against parsed/stringified JSON instead of the raw request body bytes. Fix: Pass req.rawBody (Buffer) to stripe.webhooks.constructEvent(), never JSON.stringify(req.body).

---

Real Console Error Messages

``` Error: No signatures found matching the expected signature for payload. at constructEvent (node_modules/stripe/lib/webhooks.js:67:23) ```

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

``` Error: Timestamp outside the tolerance window. ```

``` Signature mismatch: expected sig_1a2b3c, received sig_xyz789 ```

``` TypeError: Cannot read property 'toString' of undefined at verifySignature ```

---

Broken Code vs. Exact Fix

❌ BROKEN: Parsing Body Before Verification

```javascript // Express endpoint - WRONG app.post('/webhook', express.json(), (req, res) => { const sig = req.headers['stripe-signature']; try { // ❌ WRONG: req.body is already parsed object const event = stripe.webhooks.constructEvent( JSON.stringify(req.body), // ❌ Signature mismatch! sig, process.env.STRIPE_WEBHOOK_SECRET ); res.json({ received: true }); } catch (err) { res.status(400).send(Webhook Error: ${err.message}); } }); ```

✅ FIXED: Raw Body Buffer

```javascript // Express endpoint - CORRECT app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['stripe-signature']; try { // ✅ CORRECT: req.body is Buffer, passed directly const event = stripe.webhooks.constructEvent( req.body, // ✅ Raw bytes - signature verified! sig, process.env.STRIPE_WEBHOOK_SECRET ); switch (event.type) { case 'payment_intent.succeeded': console.log('Payment succeeded:', event.data.object.id); break; case 'charge.failed': console.log('Payment failed:', event.data.object.id); break; } res.json({ received: true }); } catch (err) { console.error('Webhook signature verification failed:', err.message); res.status(400).send(Webhook Error: ${err.message}); } }); ```

Why This Happens

Stripe signs the raw request body bytes. When Express parses JSON with express.json(), it converts the buffer to an object. If you then stringify it, the whitespace, key ordering, and encoding differ from the original—causing signature verification to fail.

Key difference:

  • express.json() → req.body = Object ❌
  • express.raw() → req.body = Buffer ✅
  • ---

    Step-by-Step Fix

    Step 1: Verify Endpoint Secret

    ```bash

    Check your Stripe Dashboard webhook endpoint

    Settings → Webhooks → Click endpoint → Copy "Signing secret"

    echo $STRIPE_WEBHOOK_SECRET # Should output: whsec_xxx... ```

    Step 2: Update Middleware

    Change ONE line in your webhook route:

    ```javascript // FROM THIS: app.post('/webhook', express.json(), (req, res) => {

    // TO THIS: app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { ```

    Step 3: Verify constructEvent Call

    Ensure you're passing req.body directly (it's now a Buffer):

    ```javascript const event = stripe.webhooks.constructEvent( req.body, // ✅ Buffer, not string sig, // ✅ From header process.env.STRIPE_WEBHOOK_SECRET // ✅ From env ); ```

    Step 4: Restart and Test

    ```bash

    Restart your server

    npm start

    Test with Stripe CLI (if you have local setup)

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

    ---

    Still Broken? Check These Too

    1. Environment Variable Not Set Confirm STRIPE_WEBHOOK_SECRET exists in your .env and process.env. Check Dashboard → Settings → Webhooks → Signing secret (starts with whsec_). Never use the API key by mistake.

    2. Multiple Webhook Routes Conflict If you have /webhook and /webhooks, ensure only ONE is registered in Stripe Dashboard. Multiple routes with different middleware can cause signature mismatches. [See route debugging guide](/?guide=express-routing).

    3. Webhook Secret Rotated Stripe Dashboard allows rotating signing secrets. If you regenerated it without updating .env, old signed requests will fail. Compare the whsec_ value in your code to the Dashboard.

    4. Proxy or Middleware Altering Body If using body-parser, compression middleware, or a proxy (CloudFlare, nginx), verify they're not modifying the raw request. Stripe signature depends on byte-for-byte matching.

    5. Timestamp Too Old Stripe rejects signatures older than 5 minutes by default. If your server clock is drifting, sync NTP: ntpdate -s time.nist.gov (Linux) or check macOS System Preferences → Date & Time.

    ---

    Security Note

    Always validate stripe-signature header before processing. Never trust webhook data without verification—this prevents replay attacks and spoofing.

    ---

    Resources

  • Official Docs: [Stripe Webhook Signature Verification](https://stripe.com/docs/webhooks/signatures)
  • Related: [Stripe API key configuration guide](/?guide=stripe-keys)
  • Related: [Express middleware ordering](/?guide=middleware-order)
  • ---

    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