Stripe: webhook signature verification failing [2026 fix]

Webhook signature mismatch: your endpoint secret doesn't match Stripe's signing key. Use raw request body, not parsed JSON.

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

TL;DR

Cause: You're verifying the signature against parsed JSON instead of the raw request body. Fix: Pass req.rawBody or req.body (as buffer) to stripe.webhooks.constructEvent(), never the stringified version.

---

Real Console Error Messages

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

``` StripeSignatureVerificationError: Unable to verify signature at constructEvent (/node_modules/stripe/lib/resources/WebhookEndpoints.js:123:45) ```

``` Webhook signature verification failed: {"error_code":"sig_verification_failed","message":"signature mismatch"} ```

``` Invalid signature. Expected signature: t=1704067200,v1=abc123..., got: t=1704067200,v1=def456... ```

``` Payload verification failed. Raw body required for HMAC-SHA256 validation but received stringified JSON. ```

---

The Problem: Broken vs. Fixed Code

❌ BROKEN (Most Common Issue)

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

app.post('/webhook', express.json(), (req, res) => { const sig = req.headers['stripe-signature']; const endpointSecret = process.env.STRIPE_ENDPOINT_SECRET;

try { // ❌ WRONG: Using req.body (parsed JSON object) const event = stripe.webhooks.constructEvent( req.body, // This is already parsed! Breaks HMAC verification sig, endpointSecret ); console.log('Webhook verified'); res.json({ received: true }); } catch (err) { console.error('Webhook error:', err.message); res.status(400).send(Webhook Error: ${err.message}); } }); ```

✅ FIXED (Node.js/Express)

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

// ✅ CRITICAL: Use raw body middleware ONLY for webhook route app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { const sig = req.headers['stripe-signature']; const endpointSecret = process.env.STRIPE_ENDPOINT_SECRET;

try { // ✅ CORRECT: req.body is a Buffer when using express.raw() const event = stripe.webhooks.constructEvent( req.body, // Raw buffer—HMAC signature verified correctly sig, endpointSecret ); // Handle specific events if (event.type === 'payment_intent.succeeded') { console.log('✅ Payment successful:', event.data.object.id); } res.json({ received: true }); } catch (err) { console.error('❌ Webhook error:', err.message); res.status(400).send(Webhook Error: ${err.message}); } });

// ✅ Other routes use JSON parsing normally app.post('/api/other', express.json(), (req, res) => { // req.body is parsed JSON here res.json({ ok: true }); }); ```

✅ FIXED (Next.js API Routes)

```javascript // pages/api/webhooks/stripe.js import { buffer } from 'micro'; const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

export const config = { api: { bodyParser: false, // ✅ Disable automatic JSON parsing }, };

export default async function handler(req, res) { if (req.method !== 'POST') return res.status(405).end();

const buf = await buffer(req); // ✅ Get raw buffer const sig = req.headers['stripe-signature']; const endpointSecret = process.env.STRIPE_ENDPOINT_SECRET;

try { const event = stripe.webhooks.constructEvent( buf, // ✅ Raw buffer, not JSON sig, endpointSecret ); res.status(200).json({ received: true }); } catch (err) { console.error('Webhook error:', err.message); res.status(400).end(); } } ```

---

Critical Setup Checklist

1. Endpoint Secret: Use the value from Stripe Dashboard → Developers → Webhooks (starts with whsec_), NOT your API key 2. Raw Body Required: The webhook route MUST receive raw bytes, not parsed JSON 3. Environment Variable: Confirm STRIPE_ENDPOINT_SECRET is loaded: console.log(process.env.STRIPE_ENDPOINT_SECRET) 4. Test with CLI: stripe listen --forward-to localhost:3000/webhook then trigger a test event in Dashboard 5. Version Note: Behavior is consistent in stripe@11.0.0+ (Dec 2023+), but older versions may have different signatures

---

Still Broken? Check These Too

1. [Webhook Event Type Filtering](/?guide=stripe-event-routing) — Verify you're subscribing to correct events in Stripe Dashboard. Some integrations miss charge.succeeded vs payment_intent.succeeded.

2. [Clock Skew / Timestamp Validation](/?guide=stripe-timestamp-tolerance) — Stripe rejects signatures if your server clock is >5 minutes off. Run ntpdate -s time.nist.gov (Linux) or sync time on Windows.

3. [Multiple Endpoint Secrets](/?guide=stripe-test-live-webhooks) — Accidentally using Live secret on Test webhook route (or vice versa). Dashboard shows separate secrets; confirm which one you're using.

---

Official Docs

[Stripe Webhook Signature Verification](https://stripe.com/docs/webhooks/signatures)

---

Found a different variation? Drop it in the comments—common scenarios include Firebase Cloud Functions, AWS Lambda, or custom frameworks we haven't covered.

🔥 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