Debug Stripe webhook signature verification failed locally
When Stripe says the webhook delivery succeeded but your app still fails signature verification, the problem is usually one of four things:
- the wrong signing secret
- a missing or malformed
stripe-signatureheader - an expired timestamp outside the tolerance window
- a mutated body because your framework parsed or re-serialized the request
HookLens is useful here because it captures the raw request before framework parsing, verifies the Stripe signature itself, stores the event locally, and lets you replay the exact request after you fix your app.
Start HookLens with Stripe verification
hooklens listen --port 4400 --verify stripe --secret whsec_xxxExpose 127.0.0.1:4400 with your normal tunnel or provider CLI if Stripe needs a public callback URL.
What HookLens can tell you
HookLens maps Stripe verification failures to concrete failure codes:
missing_header: thestripe-signatureheader never reached your appmalformed_header: the header was present but did not match Stripe's expected formatexpired_timestamp: the timestamp was outside the allowed verification windowsignature_mismatch: the header parsed, but the computed signature did not matchbody_mutated: the secret is likely correct, but the body bytes changed before verification
A practical debugging loop
- Run HookLens with your Stripe secret.
- Trigger the webhook delivery through Stripe.
- Check the HookLens output for
PASSorFAILand the reason. - If verification fails, run
hooklens listto inspect the stored event metadata. - Fix the middleware, secret, or route handling in your app.
- Replay the exact stored event:
hooklens replay evt_abc123 --to http://localhost:3000/webhookCommon causes
Wrong Stripe secret
The most common issue is using the wrong webhook signing secret for the endpoint you are testing. Make sure the secret passed to HookLens matches the Stripe endpoint or CLI forwarder that produced the request.
Missing or malformed stripe-signature
If HookLens reports missing_header or malformed_header, the request probably was not sent by Stripe, the header was stripped upstream, or you are testing with a plain unsigned curl request.
Expired timestamp
Stripe signs timestamp.payload. Replayed or delayed requests can fail if the timestamp falls outside the default tolerance window.
Raw body mutation
If HookLens reports body_mutated, your app likely parsed JSON before verification and changed the signed bytes. Read Why raw body mutation breaks webhook verification for the underlying reason.
Next step
If you are still narrowing down the exact cause, read the general Verification reference or the raw body guide above.