Stripe: webhook signature verification failing [2026 fix]

Webhook signature verification fails when endpoint secret is missing, misconfigured, or raw body isn't passed to verification function.

Stripe: Webhook Signature Verification Failing [2026 Fix]

TL;DR

Cause: Your code is passing the parsed JSON body instead of the raw request body to constructEvent(), or your endpoint secret is undefined/wrong. Fix: Use req.rawBody (or equivalent raw buffer) and verify STRIPE_WEBHOOK_SECRET is set in environment variables.

---

Real Console Error Messages

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

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

``` No signatures found matching the expected signature for payload. Webhook signing secret does not match. ```

``` this.signature === undefined at Object.verifyHeaderAndExtractTimestamp ```

``` stripe.webhooks.constructEvent is not a function (when using incorrect import) ```

---

Broken Code vs. Fixed Code

❌ BROKEN: Passing Parsed Body

```javascript // Express endpoint app.post('/webhook', (req, res) => { const payload = req.body; // ← WRONG: Already parsed JSON const sig = req.headers['stripe-signature']; try { // This fails because Stripe signs the raw bytes, not parsed JSON const event = stripe.webhooks.constructEvent( payload, sig, process.env.STRIPE_WEBHOOK_SECRET ); } catch (err) { res.status(400).send(Webhook Error: ${err.message}); } }); ```

✅ FIXED: Using Raw Body

```javascript // Express with raw body middleware const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const app = express();

// CRITICAL: Raw body for Stripe webhooks ONLY app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { const payload = req.body; // Now this IS the raw buffer const sig = req.headers['stripe-signature']; try { const event = stripe.webhooks.constructEvent( payload, sig, process.env.STRIPE_WEBHOOK_SECRET ); // Process event safely console.log(✓ Event verified: ${event.type}); res.status(200).json({received: true}); } catch (err) { console.error('Webhook signature verification failed:', err.message); res.status(400).send(Webhook Error: ${err.message}); } });

// Regular JSON parsing for other endpoints app.use(express.json()); ```

---

Environment & Config Checklist

Verify your .env file contains:

```bash STRIPE_WEBHOOK_SECRET=whsvc_1234567890abcdefghijklmnop STRIPE_SECRET_KEY=sk_live_xxxxxxxxxxxx ```

Verify secret is loaded:

```javascript if (!process.env.STRIPE_WEBHOOK_SECRET) { throw new Error('STRIPE_WEBHOOK_SECRET not set in environment'); } ```

Verify you're using the correct secret (not your signing secret, not API key). Get it from [Stripe Dashboard → Developers → Webhooks → Endpoint Details](https://dashboard.stripe.com/webhooks).

---

Framework-Specific Solutions

Next.js API Routes

```javascript // pages/api/webhooks/stripe.js export const config = { api: { bodyParser: false, // Disable default parser }, };

export default async function handler(req, res) { const buf = await new Promise((resolve, reject) => { let data = ''; req.on('data', chunk => { data += chunk; }); req.on('end', () => resolve(Buffer.from(data))); req.on('error', reject); }); const sig = req.headers['stripe-signature']; try { const event = stripe.webhooks.constructEvent(buf, sig, process.env.STRIPE_WEBHOOK_SECRET); res.status(200).json({received: true}); } catch (err) { res.status(400).send(Webhook Error: ${err.message}); } } ```

FastAPI/Python

```python from fastapi import Request import stripe

stripe.api_key = os.getenv('STRIPE_SECRET_KEY') endpoint_secret = os.getenv('STRIPE_WEBHOOK_SECRET')

@app.post('/webhook') async def webhook_received(request: Request): body = await request.body() # Raw bytes sig_header = request.headers.get('stripe-signature') try: event = stripe.Webhook.construct_event(body, sig_header, endpoint_secret) return {'status': 'success'} except stripe.error.SignatureVerificationError: return {'error': 'Invalid signature'}, 403 ```

---

Still Broken? Check These Too

1. Timestamp Tolerance: Stripe rejects signatures older than 5 minutes. Verify your server clock is synced. Run ntpdate -s time.nist.gov on Linux or check System Preferences → Date & Time on macOS.

2. Multiple Webhook Endpoints: You might have registered the webhook URL twice with different secrets, or you're testing against staging while code points to production. Double-check stripe.webhooks.constructEvent() receives the endpoint secret, not your API key.

3. Middleware Order Matters: If using multiple middleware, ensure express.raw() is before express.json() on the webhook route. Global express.json() will parse the body before your webhook handler runs. See [Express middleware guide](/?guide=middleware-order).

---

Test Your Webhook Locally

```bash

Use Stripe CLI to forward webhooks

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

This bypasses signature verification issues during development.

---

Version Notes

  • Stripe.js v3-v14: API stable; constructEvent() signature unchanged.
  • Stripe.js v15+: No breaking changes to webhook verification (as of 2026).
  • If you're on an older version (v8 or earlier), I'd recommend upgrading—security patches matter.
  • ---

    Official Documentation

    [Stripe Webhook Signature Verification](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