Signature verification usually fails for one of four reasons: the body was changed before hashing, the URL differs from the one the vendor signed, the parameter list is incomplete, or you are using the wrong secret. Check them in that order, and never turn verification off to get past the error.
The four usual suspects
1. The body was altered. One payment provider says it needs the raw body of the request to verify the signature, and that any manipulation of the raw body makes verification fail. Frameworks that parse JSON and re-serialize it change whitespace and key order. Read the raw bytes first, verify, then parse.
2. The URL does not match. Another vendor builds its signature from the auth token, the webhook URL configured in its console, and every request parameter. Behind a proxy or load balancer, your app may see a different scheme, host or path than the one configured. Use the public URL.
3. The parameter list is hardcoded. The same vendor warns that parameters vary by event type and may be added without notice. Validate against everything received.
4. Wrong secret. Test and live endpoints have separate signing secrets, and each endpoint has its own. A secret copied from the wrong environment fails every time. See signing secrets are not API keys.
A diagnostic table
| Symptom | Most likely cause | Check |
|---|---|---|
| Fails on every request | Wrong secret or environment | Compare endpoint and mode in the vendor console |
| Fails after adding body parsing middleware | Body altered | Log the length of the raw body and compare |
| Fails only in production | Proxy rewrites URL | Print the URL your code uses for signing |
| Fails for new event types | Strict parameter list | Pass all parameters |
| Fails intermittently on retries | Stale timestamp tolerance | Check clock sync on your server |
| Fails for forged traffic | Working as intended | Leave it alone and return an error |
One note from the payment provider's documentation: a signature verification error can occur when your integration is working correctly, for example when a third party sends fake events. Do not treat every failure as a bug.
Retries and timestamps
Some vendors generate a new signature and timestamp for each delivery attempt. If you cache a signature or reuse a timestamp, retries may fail. Verify each request independently. If you enforce a timestamp tolerance, keep server clocks synchronized.
Debug without exposing secrets
- Log the request ID, the header names present, and the body length. Do not log the secret or the full header.
- Use the vendor's CLI or test tool to send a known good event to a local endpoint.
- Use the official verification library rather than hand rolled code. Details are in how to verify a webhook signature with HMAC.
- Fix the root cause, then rerun the test.
Please do not disable the check
A temporary bypass has a way of reaching production. If you must debug in staging, use a separate environment with a separate secret, and delete the bypass before merging.
Try it yourself
Click through it at your own pace, or open it full screen. Look at how an unauthenticated endpoint appears in the results.
Quick check
Why does verification fail only behind my proxy?
Because the URL your app computes differs from the public URL the vendor signed. Use the configured public URL.
Is it safe to skip verification in staging?
Avoid it. Use a separate staging secret and the vendor's test tools so the same code path runs everywhere.
Sources
- Receive events in your webhook endpoint, Stripe documentation
- Error handling, Stripe documentation
- Secure webhooks, Twilio documentation
Last reviewed: 19 October 2026.