Documentation Menu

Stripe "No Signatures Found Matching" Error — Complete Fix

Problem Introduction

Stripe throws "No signatures found matching the expected signature for payload" when constructEvent() receives a mutated body.

Why It Happens

  • express.json() middleware parsed the body before express.raw() ran on the webhook route
  • Using sk_live_ API key instead of whsec_ webhook signing secret
  • Extra quotes around STRIPE_WEBHOOK_SECRET in .env file
  • Timestamp replay window exceeded (>5 minutes)

Step-by-Step Fix

  1. 1Pass the exact raw Buffer/String to stripe.webhooks.constructEvent() — never parsed JSON.
  2. 2Confirm your secret starts with "whsec_" — found at Dashboard → Webhooks → Signing secret.
  3. 3Remove extra quotes from .env: STRIPE_WEBHOOK_SECRET=whsec_xxx (no surrounding quotes).
  4. 4For expired timestamps (>5 min old), use Stripe CLI for local testing instead of Dashboard replays.
  5. 5Never call JSON.stringify(req.body) to reconstruct the payload — it reorders keys.

Working Code

Copy-paste verified examples. Use the tab that matches your stack.

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

// express.raw() must run BEFORE express.json() — apply only to webhook route
app.post(
  '/webhooks/stripe',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const sig = req.headers['stripe-signature'];
    let event;

    try {
      event = stripe.webhooks.constructEvent(
        req.body,                              // must be raw Buffer
        sig,
        process.env.STRIPE_WEBHOOK_SECRET      // whsec_...
      );
    } catch (err) {
      // "No signatures found"  → raw body was parsed (use express.raw)
      // "Timestamp outside tolerance" → replay expired; use Stripe CLI
      console.error('Stripe error:', err.message);
      return res.status(400).send('Webhook Error: ' + err.message);
    }

    switch (event.type) {
      case 'payment_intent.succeeded':
        await fulfillOrder(event.data.object);
        break;
      case 'customer.subscription.deleted':
        await cancelSubscription(event.data.object);
        break;
    }

    res.json({ received: true });
  }
);

Common Mistakes

  • Using express.json() before express.raw() on the webhook route
  • Using sk_test_ or sk_live_ key instead of whsec_ secret
  • Calling JSON.stringify on req.body to rebuild the string

Debugging Workflow

Receive raw bytes → pass to Stripe SDK → handle event type → return 200.

Preventive Best Practices

  • Use Hookmetry to instantly diagnose structural anomalies in Stripe payloads

Works with webhooks and other async event systems (including AI callbacks). Instead of guessing, inspecting the exact payload and headers can help debug faster.

Try the free webhook tester

Was this page helpful?

Your feedback helps us improve the docs.