Stripe: webhook signature verification failing [2026 fix]

Webhook signature mismatch due to raw body tampering or wrong endpoint secret—use raw request body and correct secret key immediately.

Stripe: Webhook Signature Verification Failing [2am SOS]

TL;DR

Cause: Your webhook handler is verifying against a parsed/stringified body instead of the raw request stream, OR you're using the wrong endpoint secret. Fix: Pass the raw unparsed request body buffer to stripe.webhooks.constructEvent() and verify you're using your *endpoint's* secret (not your API key).

---

Real Console Error Messages

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

2. StripeSignatureVerificationError: Unable to extract timestamp and signatures from header stripe-signature

3. Error: Webhook Error: invalid payload signature

4. TypeError: Cannot read property 'id' of undefined (downstream, triggered by failed verification)

5. 403 Forbidden - Stripe webhook signature verification failed ```

---

The Problem: Side-by-Side Code Comparison

❌ BROKEN (Most Common)

```javascript // Express.js with body-parser (DEFAULT SETUP) const express = require('express'); const bodyParser = require('body-parser'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const app = express(); // THIS IS THE KILLER: body-parser JSON middleware parses the body app.use(bodyParser.json());

app.post('/webhook', (req, res) => { // req.body is NOW a parsed JavaScript object, NOT raw bytes // Stripe's signature is computed against the original raw bytes // These don't match → verification fails const event = stripe.webhooks.constructEvent( req.body, // 🚨 WRONG: This is parsed JSON, not raw body req.headers['stripe-signature'], process.env.STRIPE_ENDPOINT_SECRET ); res.sendStatus(200); }); ```

✅ FIXED (Correct Implementation)

```javascript // Express.js - CORRECT approach const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const app = express();

// CRITICAL: Express raw body middleware BEFORE other parsers app.use(express.raw({type: 'application/json'}));

// NOW apply JSON parsing to everything EXCEPT /webhook app.use((req, res, next) => { if (req.path === '/webhook') { return next(); } express.json()(req, res, next); });

app.post('/webhook', (req, res) => { // req.body is now a Buffer containing raw bytes // This matches Stripe's signature computation const sig = req.headers['stripe-signature']; let event; try { event = stripe.webhooks.constructEvent( req.body, // ✅ CORRECT: Raw buffer sig, process.env.STRIPE_ENDPOINT_SECRET // ✅ Use ENDPOINT secret, not API key ); } catch (err) { console.error('Webhook Error:', err.message); return res.sendStatus(400); }

// Process event safely console.log(Received event: ${event.type}); res.sendStatus(200); }); ```

Alternative: Fastify Fix

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

const app = fastify();

// Fastify: Tell it NOT to parse webhook route app.register(require('@fastify/raw-body'), { routes: ['/webhook'] });

app.post('/webhook', async (request, reply) => { // request.rawBody contains the raw bytes const event = stripe.webhooks.constructEvent( request.rawBody, request.headers['stripe-signature'], process.env.STRIPE_ENDPOINT_SECRET ); reply.code(200).send({ ok: true }); }); ```

---

Critical Checklist

1. Environment Variable Verification: ```bash # In your .env or hosting platform: STRIPE_ENDPOINT_SECRET=whsec_test_4eC39HqLyjWDarhtT657j8saw # NOT your API key STRIPE_SECRET_KEY=sk_test_4eC39HqLyjWDarhtT657j8saw ``` Get the endpoint secret from [Stripe Dashboard → Webhooks → Your Endpoint → Signing Secret]

2. Confirm raw body is Buffer: ```javascript app.post('/webhook', (req, res) => { console.log('Body type:', typeof req.body, 'Is Buffer:', Buffer.isBuffer(req.body)); // Should output: "Body type: object Is Buffer: true" }); ```

3. Middleware Order Matters: - express.raw() MUST come before express.json() for this route

---

Still Broken? Check These Too

1. [Webhook Endpoint Not Registered Properly](/?guide=stripe-webhook-registration) — Verify endpoint exists in Dashboard and is enabled. Test button works but curl fails? Check firewall/network ACLs blocking Stripe IP ranges (35.184.0.0/13).

2. [Stripe.js vs @stripe/stripe-js Version Mismatch](/?guide=stripe-version-conflict) — Using stripe npm package v8 with Node 14? Explicit compatibility check required. Run npm ls stripe and cross-reference [official SDK changelog](https://github.com/stripe/stripe-node/releases).

3. Clock Skew Issues — Stripe rejects signatures older than 5 minutes. If your server time is off by >300s, verification fails silently. Check: date command on server vs NTP.

---

Official References

  • [Stripe Webhooks Documentation](https://stripe.com/docs/webhooks)
  • [Stripe Node.js SDK - constructEvent](https://github.com/stripe/stripe-node#webhooks)
  • [Express Raw Body Middleware](https://expressjs.com/en/api/express.html#express.raw)
  • ---

    Found a different variation? Drop it in the comments — Common setups: Next.js API routes, AWS Lambda, Google Cloud Functions, or custom request body buffering strategies. Your fix might save someone 2 hours of debugging at 2am.

    🔥 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