Error Medic

Troubleshooting 'Stripe Webhook Signature Verification Failed' Errors

Resolve 'Stripe webhook signature verification failed' errors. Learn how to capture raw request bodies, verify endpoint secrets, and test locally.

Last updated:
Last verified:
1,426 words
Key Takeaways
  • The number one cause is framework middleware parsing the request payload into a JSON object instead of preserving the raw string/buffer.
  • Ensure you are using the correct Webhook Endpoint Secret (starts with whsec_), not your standard API Secret Key (starts with sk_).
  • Test and Live environments, as well as the Stripe CLI, all have completely distinct webhook secrets.
  • Quick Fix: Disable global JSON parsing middleware for your webhook route and use framework-specific raw body parsers (e.g., express.raw()).
Raw Body Capture Methods Compared by Framework
FrameworkMethod / MiddlewareTime to FixRisk
Express.js (Node)express.raw({type: 'application/json'})5 minsLow (if scoped only to the webhook route)
FastAPI (Python)payload = await request.body()5 minsLow
Django (Python)payload = request.body5 minsLow
Go (net/http)body, err := io.ReadAll(r.Body)5 minsLow

Understanding the Error

When integrating Stripe webhooks, one of the most common and frustrating roadblocks developers encounter is the StripeSignatureVerificationError (or its equivalent in other languages, such as stripe.error.SignatureVerificationError in Python). The exact error message usually looks like this:

StripeSignatureVerificationError: No signatures found matching the expected signature for payload. Are you passing the raw request body you received from Stripe?

Or simply: Webhook signature verification failed.

This error is a security mechanism working exactly as intended. Stripe signs the webhook events it sends to your endpoints by including a Stripe-Signature header. This header contains a timestamp and one or more signatures that you must verify using your endpoint's unique webhook secret (whsec_...).

The signature verification process serves two critical security functions:

  1. Authentication: It proves that the request actually originated from Stripe and not from a malicious third party scanning your exposed API endpoints.
  2. Data Integrity: It guarantees that the payload has not been intercepted and altered in transit.

If the verification fails, it means the hash of the payload your server computed does not match the hash Stripe sent. In 95% of cases, this is not a bug in the Stripe SDK, but rather an issue with how your server is handling the incoming HTTP request.

Root Cause 1: The "Raw Body" Problem (The 95% Case)

The most frequent cause of the StripeSignatureVerificationError is that your web framework's middleware is parsing the incoming JSON payload and mutating it before it reaches the Stripe SDK's verification function.

Cryptographic hashes are incredibly sensitive. If Stripe signs a payload that looks like {"amount": 1000} (with a single space after the colon), and your server parses it into an object and then re-serializes it as {"amount":1000} (without the space), the resulting HMAC SHA-256 hashes will be completely different.

Modern web frameworks (like Express.js, FastAPI, Django, or Spring Boot) often have global middleware configured to automatically parse incoming application/json requests. By the time the request reaches your webhook handler, the "raw" string body is gone, replaced by a parsed dictionary or object.

Express.js (Node.js) Example

If you have app.use(express.json()); applied globally before your webhook route, the verification will fail. You must capture the raw body specifically for the Stripe webhook route.

FastAPI (Python) Example

In FastAPI, simply defining a Pydantic model for the incoming request will consume the raw body. You need to read the raw bytes using await request.body().

Root Cause 2: Incorrect Webhook Secret

Every webhook endpoint you register in the Stripe Dashboard (or via the API) is assigned a unique signing secret. This secret begins with whsec_.

Common mistakes include:

  • Environment Mismatch: Using your Test Mode webhook secret (whsec_test_...) while receiving Live Mode events, or vice versa.
  • Using the Wrong Key: Using your Stripe API Secret Key (sk_test_... or sk_live_...) instead of the Endpoint Secret (whsec_...).
  • Multiple Endpoints: Registering multiple webhook endpoints in the Stripe dashboard and accidentally swapping their secrets in your environment variables.

Root Cause 3: Timestamp and Tolerance Issues

The Stripe-Signature header contains a timestamp (t=). When the Stripe SDK verifies the signature, it checks this timestamp against your server's current system clock. By default, the SDK allows a tolerance of 5 minutes (300 seconds).

If your server's clock is significantly out of sync with NTP (Network Time Protocol) servers, the verification will fail with an error like:

StripeSignatureVerificationError: Timestamp outside the tolerance zone

This mechanism prevents Replay Attacks, where an attacker intercepts a valid webhook request and attempts to re-send it to your server hours or days later to trigger duplicate actions (e.g., fulfilling an order multiple times).


Step-by-Step Troubleshooting and Fixes

Step 1: Verify Your Keys and Environment Variables

Before diving into code, definitively rule out credential issues:

  1. Log in to the Stripe Dashboard.
  2. Navigate to Developers > Webhooks.
  3. Select the specific endpoint URL that is failing.
  4. Click Reveal under the "Signing secret" section.
  5. Compare this whsec_... value exactly with the environment variable loaded in your application (e.g., STRIPE_WEBHOOK_SECRET). Ensure there are no trailing whitespaces or newline characters in your .env file.

Step 2: Implement Raw Body Capture

You must modify your routing or middleware to preserve the exact raw bytes of the incoming request.

For Node.js / Express: Do not apply express.json() globally if it affects your webhook route. Instead, use express.raw({type: 'application/json'}) specifically for the Stripe endpoint.

For Python / FastAPI: Extract the raw body using Request.body(). Do not define a Pydantic model for the incoming request body on this specific endpoint.

Step 3: Synchronize System Clocks

If you are receiving timestamp tolerance errors, ensure your server is running an NTP daemon (like chronyd or systemd-timesyncd).

On standard Linux distributions (Ubuntu/Debian), verify time synchronization by running timedatectl status in your terminal. Ensure that "NTP service: active" and "System clock synchronized: yes" are both present. If not, restart the timesync daemon.

Step 4: Test Locally with the Stripe CLI

The best way to develop and debug webhooks is using the official Stripe CLI. It allows you to forward events directly to your local development environment and generates a local webhook secret.

  1. Start forwarding: stripe listen --forward-to localhost:3000/api/webhooks/stripe
  2. CRITICAL: The CLI will output a local webhook secret (whsec_...). You MUST use this secret in your local .env file, not the one from the Stripe Dashboard.
  3. Trigger an event in a separate terminal: stripe trigger payment_intent.succeeded

Watch your application logs. If the signature verifies successfully locally but fails in production, the issue is almost certainly related to how your production load balancer, reverse proxy (e.g., Nginx, Cloudflare), or API gateway is modifying the request body or headers before it reaches your application container.

Frequently Asked Questions

bash
# 1. Start the Stripe CLI to forward events to your local server
# Make note of the 'whsec_...' secret output by this command
stripe listen --forward-to localhost:3000/api/webhooks/stripe

# 2. In a separate terminal window, trigger a test event
stripe trigger payment_intent.succeeded

# 3. Check NTP status if you suspect a timestamp/clock drift issue (Linux)
timedatectl status

# Restart the time synchronization daemon if necessary
sudo systemctl restart systemd-timesyncd
E

Error Medic Editorial

Authored by senior Site Reliability Engineers and DevOps practitioners dedicated to solving complex infrastructure and API integration challenges.

Sources

Related Articles in Stripe

Explore More API Errors Guides