Stripe: webhook signature verification failing [2026 fix]

Your webhook secret is mismatched or endpoint isn't receiving raw request body. Move to Stripe CLI or verify signing secret matches webhook endpoint config.

Stripe: Webhook Signature Verification Failing [2026 Fix]

TL;DR

Cause: You're signing the request with one secret but Stripe is sending webhooks signed with a different secret, OR your framework is parsing JSON before signature verification runs.

Fix: Use the *endpoint secret* (not API key) from your Stripe Dashboard webhook settings, and verify signatures BEFORE parsing the request body.

---

Real Console Error Messages

Here are exact errors you'll see at 2am:

``` Error: No signatures found matching the expected signature for payload. Webhook secret (signing secret) may be incorrect. ```

``` StripeSignatureVerificationError: Timestamp outside the tolerance zone at verifySignatureWithMethod (/node_modules/stripe/lib/utils.js:156:23) ```

``` Error: Invalid signature. Expected sig=[hash], but request signature was sig=[different_hash] ```

``` Webhook Error: Unable to verify webhook signature for endpoint /webhook/stripe Signature: t=1704067200,v1=abc123... Secret mismatch detected ```

``` TypeError: Cannot read property 'id' of undefined at verifySignature (stripe.js:45:12) ```

---

The Problem: Broken vs Fixed Code

❌ BROKEN (Most Common)

```javascript // Express.js endpoint app.post('/webhook/stripe', express.json(), (req, res) => { const sig = req.headers['stripe-signature']; const secret = process.env.STRIPE_API_KEY; // ← WRONG! Use webhook secret try { const event = stripe.webhooks.constructEvent( JSON.stringify(req.body), // ← WRONG! Body already parsed by middleware sig, secret ); res.json({received: true}); } catch (err) { console.log('Webhook error:', err.message); res.status(400).send(Webhook Error: ${err.message}); } }); ```

Three problems here: 1. Using STRIPE_API_KEY instead of STRIPE_WEBHOOK_SECRET 2. express.json() middleware parsed the body before verification 3. Passing stringified already-parsed body breaks signature verification

✅ FIXED

```javascript // Express.js endpoint - CORRECT app.post( '/webhook/stripe', express.raw({type: 'application/json'}), // ← Raw buffer, NOT parsed (req, res) => { const sig = req.headers['stripe-signature']; const secret = process.env.STRIPE_WEBHOOK_SECRET; // ← Correct secret try { const event = stripe.webhooks.constructEvent( req.body, // ← Raw buffer from express.raw() sig, secret ); // Handle events 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}); } } ); ```

What changed:

  • express.json()express.raw({type: 'application/json'}) keeps body as raw Buffer
  • STRIPE_API_KEYSTRIPE_WEBHOOK_SECRET (find in Dashboard > Developers > Webhooks)
  • Removed JSON.stringify()
  • For Next.js / Vercel

    ```javascript export const config = { api: { bodyParser: false, // ← Critical: disable auto-parsing }, };

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

    async function buffer(readable) { const chunks = []; for await (const chunk of readable) { chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk); } return Buffer.concat(chunks); } ```

    ---

    Still Broken? Check These Too

    1. Timestamp tolerance exceeded — Stripe rejects signatures older than 5 minutes. Check your server clock is synchronized (run ntpdate -s time.nist.gov on Unix). Webhook was sent at t=1704067200 but your server timestamp is drifted.

    2. Wrong environment — You added the webhook URL to your *test* Stripe account but your code uses STRIPE_WEBHOOK_SECRET from *production*. Toggle between test/live mode in Dashboard. Use separate environment variables: STRIPE_WEBHOOK_SECRET_TEST vs STRIPE_WEBHOOK_SECRET_LIVE.

    3. Proxy or firewall stripping headers — CloudFlare, nginx, or API Gateway might strip stripe-signature headers. Verify the header reaches your endpoint by logging: console.log('Headers:', req.headers) before verification. If missing, whitelist the header in your proxy config.

    ---

    Quick Verification Checklist

  • [ ] Dashboard > Developers > Webhooks: copy exact secret value
  • [ ] Confirm secret starts with whsec_test_ or whsec_live_
  • [ ] Verify you're NOT using your API key (starts sk_test_ or sk_live_)
  • [ ] Check middleware chain: raw body BEFORE signature verification
  • [ ] Confirm stripe-signature header is present in logs
  • [ ] Test with [Stripe CLI](https://stripe.com/docs/stripe-cli): stripe listen --forward-to localhost:3000/webhook/stripe
  • ---

    Related Guides

  • [Fixing Firebase function timeouts with Stripe async webhooks](/?guide=firebase-stripe-timeout)
  • [Express error handling patterns for payment flows](/?guide=express-payment-errors)
  • ---

    Official Reference

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

    ---

    Found a different variation? Drop it in the comments.

    Stripe updates their client libraries regularly. If your error message doesn't match these but involves webhook signatures, describe: (1) your framework version, (2) your exact error output, and (3) your signing code. We'll add it to this guide.

    🔥 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