WooCommerce

Stripe Webhooks in WooCommerce: Orders Stuck On Hold

Payments succeed in Stripe but WooCommerce orders stay on hold. How the webhook flow works, why signatures fail, and a diagnostic that finds the break fast.

5 min read
WooCommerceStripeCheckout
814 words5 min read

The symptom

Customers are charged. Stripe's dashboard shows successful payments. WooCommerce shows the same orders as Pending payment or On hold, stock is not reduced, confirmation emails do not go out, and someone at the store is manually marking orders as processing every morning.

The payment worked. The message from Stripe telling WooCommerce that it worked did not arrive, or arrived and was rejected. That message is a webhook, and this is how to find where it broke.

How the flow is supposed to work

  1. The customer submits the checkout. WooCommerce creates an order in Pending status and asks Stripe to create a PaymentIntent.
  2. The customer's card is authorised and captured by Stripe, often via 3D Secure.
  3. Stripe sends a webhook, payment_intent.succeeded or charge.succeeded, to the store's webhook endpoint.
  4. The WooCommerce Stripe plugin verifies the webhook's signature, finds the matching order, and moves it to Processing or Completed. Stock reduces, emails send.

Steps one and two are visible in Stripe. Step four is what the store sees. Step three is the part nobody looks at, and it is nearly always where the fault is.

Diagnostic sequence

Check Stripe's webhook log first. In the Stripe dashboard, under Developers, Webhooks, open the endpoint for the store. Stripe lists every delivery attempt with its response code. This tells you immediately which of three situations you are in:

  • No deliveries at all. The endpoint is not configured, or is configured on the wrong Stripe account or mode (test versus live). Common after a site migration or after switching from test to live keys.
  • Deliveries with non-200 responses. The store is receiving the webhook and rejecting it. The response body Stripe shows tells you why.
  • Deliveries with 200 responses but orders still on hold. The store accepted the webhook and did nothing useful with it. Rarer, and usually a plugin conflict or a mismatch between the order and the PaymentIntent.

If there are no deliveries: re-run the plugin's webhook configuration, which registers the endpoint with Stripe for the current keys, and confirm the mode matches. Check that the endpoint URL uses the live domain, not staging.

If deliveries are rejected: read the response.

Why signatures fail

The most common rejection is a signature verification failure. Stripe signs each webhook with the endpoint's signing secret; the plugin verifies it. Failures come from:

  • The wrong signing secret. The endpoint was recreated in Stripe and the plugin still has the old secret. Copy the current secret from the endpoint's details into the plugin settings.
  • Multiple endpoints. Two endpoints in Stripe pointing at the same store, from a migration or a previous developer, each with its own secret. Only one matches. Delete the stale one.
  • A proxy or security plugin modifying the request body. Signatures are computed over the raw body. A firewall, a caching layer or a plugin that rewrites the request breaks it. Exclude the webhook URL from anything that touches request bodies.
  • Clock skew. Stripe rejects timestamps too far from its own. A server with a badly wrong clock fails every signature.

Why deliveries get blocked

Non-signature rejections are usually access problems:

  • A security plugin or WAF blocking POST requests from Stripe's IP ranges, or blocking requests with no browser user-agent. Whitelist the webhook path.
  • Basic auth or a coming-soon plugin on staging that is still active on production.
  • The REST API disabled by a hardening plugin. The Stripe endpoint depends on it.
  • A caching layer returning a cached response to a POST. Exclude the webhook path from page caching.

How retries behave

Stripe retries failed webhooks with increasing delays over roughly three days, then gives up. That is why orders from yesterday sometimes fix themselves overnight when the block is removed, and why orders from last week do not. For orders that missed the window, the plugin's dashboard usually offers a way to re-check status against Stripe, or the order can be reconciled manually from the PaymentIntent.

Testing after a fix

Use Stripe's "Send test webhook" button on the endpoint, and watch the store's response in the same log. Then place a real low-value order in live mode and confirm it moves to Processing without anyone touching it. Check the order note: the plugin writes a line when it handles a webhook, and that line is the proof.

Preventing it

Webhook failures almost always follow a change: a migration, new security plugin, key rotation, a firewall rule. Add "place a test order and confirm it processes" to the post-change checklist. On a care plan, a monthly test order on a store is cheap insurance, and it is one of the checks in our WooCommerce work, because a store that is charging cards and not fulfilling orders is the most expensive quiet failure a shop can have.

All writingHire me for this