The Problem
A client pinged me on Monday because Stripe was pouring failed webhooks into the dashboard with the same red status: 400 Invalid signature. Orders were paying through on the front end, but subscription renewals, refund syncs, and order status transitions stopped working. The logs inside WooCommerce → Status → Logs → stripe-webhook showed the same line every few minutes:
Webhook signature verification failed. No signatures found matching the expected signature for payload.
The update happened overnight. The WooCommerce Stripe Gateway jumped a minor version, WordPress auto-updated, and suddenly Stripe was shouting. Nothing else on the server had changed.
If you are seeing the same pattern, the cause is almost always one of three things. None of them require disabling signature verification.
Why It Happens
Stripe signs every webhook with the secret that belongs to the exact endpoint you registered in the dashboard. The gateway checks three things when a request arrives:
- The raw, unmodified request body
- The
Stripe-Signatureheader - The endpoint signing secret stored in WooCommerce
If any of those three is mutated, rewritten, or replaced, verification fails. After the WooCommerce Stripe 9.x release, I see this fail for four common reasons:
1. The secret is from the wrong endpoint. Stripe now strongly prefers per-endpoint secrets over the legacy account-wide key. If your site has two endpoints registered (one from an old plugin, one from the new /wc-api/wc_stripe route) you can easily paste the wrong one into WooCommerce.
2. The raw body was mangled. A caching plugin, a security plugin, or a reverse proxy stripped JSON, re-encoded it as UTF-8 BOM, or injected whitespace. Signature verification runs against the exact byte stream Stripe sent.
3. A Cloudflare or WAF rule rewrote headers. Some Cloudflare managed rules normalize Stripe-Signature casing or drop it entirely on suspicious traffic. The request reaches WooCommerce, but the header is gone.
4. The endpoint URL changed. After moving to HPOS or switching from /?wc-api=wc_stripe to the REST route in newer gateway versions, the secret tied to the old URL stops matching the new endpoint path.
The Fix
Work through these in order. Ninety per cent of the sites I look at are fixed before step 3.
1. Re-pair the endpoint and secret
In the Stripe Dashboard, open Developers → Webhooks and look at the active endpoints. Remove any stale ones from older plugin versions. Open the endpoint that matches your current gateway route, click Reveal signing secret, and paste it into WooCommerce → Payments → Stripe → Webhook Secret.
The current endpoint on recent gateway versions is:
https://example.com/wp-json/wc/v3/stripe/webhook
Not the legacy /?wc-api=wc_stripe URL. If your Stripe dashboard still lists the old URL, add a new endpoint for the REST route and copy its secret. Old secret against new endpoint is the single most common cause.
2. Send a test event and read the log carefully
In Stripe, click Send test webhook → payment_intent.succeeded. Then in WooCommerce:
WooCommerce → Status → Logs → stripe-webhook-YYYY-MM-DD
If the error text says No signatures found matching, it is a secret mismatch. If it says Timestamp outside the tolerance zone, your server clock is drifting. Fix that with:
sudo timedatectl set-ntp true
sudo systemctl restart systemd-timesyncd
A drift of more than five minutes is enough to break Stripe's replay protection.
3. Stop middleware from touching the body
If the secret is correct and the clock is fine, something is mutating the request. Add this small drop-in to wp-content/mu-plugins/stripe-raw-body.php:
<?php
/**
* Capture raw request body for Stripe signature verification
* before any plugin pre-parses JSON.
*/
add_action( 'init', function () {
if ( empty( $_SERVER['REQUEST_URI'] ) ) {
return;
}
if ( strpos( $_SERVER['REQUEST_URI'], '/stripe/webhook' ) === false ) {
return;
}
if ( ! defined( 'WC_STRIPE_RAW_BODY' ) ) {
define( 'WC_STRIPE_RAW_BODY', file_get_contents( 'php://input' ) );
}
}, 0 );
This grabs the raw body before any security plugin or JSON parser runs. The gateway will pick it up from WC_STRIPE_RAW_BODY on recent versions. If you use a plugin like Wordfence or iThemes that normalises JSON payloads, add the webhook URL to its allow list so it skips the request entirely.
4. Bypass Cloudflare for the endpoint
In Cloudflare, open Rules → Configuration Rules and add a rule:
URI Path contains "/stripe/webhook"
Then: Disable Security, Disable Performance, Cache Level: Bypass
That keeps the raw body and the Stripe-Signature header intact all the way to origin. You can keep full protection on the rest of the site.
5. Re-check on a test transaction
Run a one-dollar test charge, then trigger a refund from Stripe. Both events should land as 200 in the webhook log and the order status should update in WooCommerce without touching it manually. For more on why block-based checkout adds its own wrinkles to this flow, my write-up on WooCommerce block checkout payment methods covers the frontend side.
The canonical reference for the signature format is in Stripe's webhook signing docs. Worth keeping open in a tab while you debug.
What Not To Do
Do not disable signature verification. I have seen WC_STRIPE_DISABLE_SIGNATURE_VERIFICATION set to true as a "quick fix" on production. That opens the site to webhook spoofing — anyone who guesses the endpoint URL can post fake payment_intent.succeeded events and force orders into completed status. The verification is not the problem; a misconfigured pipeline is.
Do not rotate the signing secret on every failure either. Rotating changes the symptom briefly, then the same middleware eats the next request and you are back where you started. Fix the underlying cause first, rotate only if the old secret is actually compromised.
Need This Handled on a Live Store?
I fix production WooCommerce payment pipelines: Stripe, subscriptions, webhooks, and the messy middleware layer that breaks them after every plugin update. If your store is dropping renewals or silently missing refunds, see my WordPress and WooCommerce services and I will get the webhook pipeline stable without weakening the verification layer.