Stripe: webhook signature verification failing [2026 fix]

Webhook signature verification fails when endpoint secret is missing or mismatched; use correct env var and validate raw request body.

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

TL;DR

Cause: You're using the wrong endpoint secret or passing processed JSON instead of raw request body to verifyWebhookSignature(). Fix: Load STRIPE_WEBHOOK_SECRET from environment, pass req.rawBody (not req.body), and ensure your framework doesn't auto-parse webhook requests.

---

Exact Console Errors You'll See

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

``` Stripe.error.SignatureVerificationError: No signatures found matching the expected signature for payload. Webhook id: evt_xxxxx ```

``` webhook signature verification failed: computed sig does not match stripe-signature header ```

``` Error: Invalid signature. Expected sig: sha256=xxx, got: sha256=yyy ```

``` TypeError: Cannot read property 'rawBody' of undefined at Object.constructEvent ```

---

Broken Code vs. Exact Fix

❌ BROKEN: Express with auto-parsed body

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

// Problem: This parses JSON, destroying the raw signature app.use(express.json());

app.post('/webhook', (req, res) => { const sig = req.headers['stripe-signature']; const endpointSecret = 'whsec_test_secret'; // WRONG: hardcoded try { // WRONG: passing parsed req.body const event = stripe.webhooks.constructEvent( req.body, // ← This is already parsed JSON, signature won't verify sig, endpointSecret ); res.json({received: true}); } catch (err) { res.status(400).send(Webhook error: ${err.message}); } }); ```

✅ FIXED: Webhook-specific raw body handling

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

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

// CRITICAL: Webhook route BEFORE global middleware to capture raw body app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { const sig = req.headers['stripe-signature']; const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; // ← Load from env if (!endpointSecret) { return res.status(400).send('Missing STRIPE_WEBHOOK_SECRET environment variable'); } try { // CORRECT: passing raw buffer const event = stripe.webhooks.constructEvent( req.body, // ← Now this is Buffer/raw body sig, endpointSecret ); // Handle event switch (event.type) { case 'payment_intent.succeeded': console.log('Payment succeeded:', event.data.object.id); break; case 'charge.refunded': console.log('Charge refunded:', event.data.object.id); break; } res.json({received: true}); } catch (err) { console.error('Webhook signature verification failed:', err.message); return res.status(400).send(Webhook error: ${err.message}); } });

// IMPORTANT: Put other routes after webhook app.post('/api/other', express.json(), (req, res) => { // This gets normal parsed JSON res.json({ok: true}); });

app.listen(3000); ```

❌ BROKEN: Next.js API Route

```javascript // pages/api/webhook.js import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

export default async function handler(req, res) { if (req.method !== 'POST') return res.status(405).end(); const sig = req.headers['stripe-signature']; // WRONG: Next.js auto-parses body const event = stripe.webhooks.constructEvent( req.body, // ← Already parsed sig, 'whsec_test123' ); }

export const config = { api: { bodyParser: false }, // Forgot this }; ```

✅ FIXED: Next.js API Route

```javascript // pages/api/webhook.js import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

export default async function handler(req, res) { if (req.method !== 'POST') return res.status(405).end(); const sig = req.headers['stripe-signature']; const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; try { const event = stripe.webhooks.constructEvent( req.body, // ← Correctly raw buffer due to config below sig, endpointSecret ); res.status(200).json({received: true}); } catch (err) { return res.status(400).send(Webhook Error: ${err.message}); } }

// CRITICAL: Disable body parsing for this route export const config = { api: { bodyParser: false, }, }; ```

---

Environment Variable Checklist

✓ STRIPE_WEBHOOK_SECRET is in your .env.local (starts with whsec_) ✓ You copied it from [Stripe Dashboard > Webhooks > Signing Secret](https://dashboard.stripe.com/webhooks) ✓ Secret is different per environment (test vs. live) ✓ Not hardcoded in source

---

Still Broken? Check These Too

1. Webhook URL mismatch: Your registered endpoint in Stripe dashboard (https://api.example.com/webhook) must exactly match your actual server URL. Check [related: domain routing](/?guide=webhook-domain-routing).

2. Stripe CLI testing: If testing locally with stripe listen, the CLI generates a different signing secret than dashboard webhooks. Don't mix them. Use stripe listen --print-secret output for local .env.

3. Timestamp validation: Stripe rejects signatures older than 5 minutes. If server clock is skewed >300s, verification fails. Sync NTP: ntpdate -s time.nist.gov (Linux) or check System Preferences (Mac).

4. Proxy/reverse proxy stripping headers: If behind nginx/CloudFlare, ensure stripe-signature header reaches your app. Check [related: headers through proxies](/?guide=proxy-header-passthrough).

5. Race condition on secret rotation: If you rotated webhook secrets in Stripe dashboard, old requests in flight may still arrive. Both old+new secrets remain valid for ~24 hours.

---

Version Notes

Stripe Node.js SDK: This guide covers stripe >= 8.0. Earlier versions used different method names (not confirmed for 2026, verify with npm list stripe).

Express versions: express >= 4.16.0 includes express.raw(). For older versions, use body-parser separately.

---

Official Resources

  • [Stripe Webhook Signature Verification Docs](https://stripe.com/docs/webhooks/signatures)
  • [Stripe Node.js SDK GitHub](https://github.com/stripe/stripe-node)
  • ---

    Found a different variation? Drop it in the comments. (Cloudflare Workers? Deno? Lambda with API Gateway? Share your fix!)

    🔥 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