This exact error means Stripe's SDK re-computed the HMAC signature from the body it received and it did not match the Stripe-Signature header. It is almost never a wrong secret. In order of how often each one is actually the cause:
1. Something parsed the body before you verified it
Any JSON-parsing middleware that runs globally (express.json(), app.use(bodyParser.json()), a framework's default request parser) re-serializes the body. The bytes you verify are no longer the exact bytes Stripe sent, so the signature never matches.
Fix: exempt the webhook route from body parsing, or capture the raw body before any parser touches it.
Express:
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const event = stripe.webhooks.constructEvent(req.body, req.headers['stripe-signature'], endpointSecret);
});
Flask: read request.get_data() (raw bytes), never request.get_json(), before calling construct_event.
2. Test-mode secret used against a live-mode event, or vice versa
Each endpoint (we_...) has its own whsec_... signing secret, and test mode and live mode are entirely separate endpoints with different secrets. Pointing your live webhook URL at a secret copied from the test-mode dashboard fails every time. Check which mode the event actually came from (livemode field once you skip verification once, just to look) against which secret is loaded.
3. A proxy or CDN is rewriting the body
Some reverse proxies (misconfigured gzip, some WAFs) modify the payload in transit. If (1) and (2) are both correct and it still fails, log the raw body length and byte content at the handler and compare it to what Stripe's dashboard shows was sent in that delivery's request payload.
4. You're re-signing after a redirect or serverless cold-path rewrite
Some serverless platforms (older Next.js API routes, some Vercel configs) parse the body automatically before your function runs, same root cause as (1) but harder to see because it's platform config, not your code. Look for bodyParser: false / config.api.bodyParser type settings.
If you want a second pair of eyes on your actual webhook handler (not a generic checklist), I do a $39 done-for-you audit of a Stripe integration: https://saasfactory.netlify.app/audit.html — or grab the $9 self-serve compliance checklist: https://buy.stripe.com/eVq14m0WF0LU2TVdRS7IY01
Top comments (0)