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