How I Traced 23 Missing WooCommerce Orders to a Broken Stripe Webhook

· 14 min read

The Client Who Was Paid but Didn't Know It

A WooCommerce store owner messaged me on a Monday morning: "Customers are emailing saying they paid but never got a confirmation. I can see the money in Stripe. WooCommerce shows the orders as Pending payment."

This wasn't a payment gateway failure in the traditional sense. Customers were completing checkout, Stripe was charging their cards successfully, and the money was landing in the merchant's Stripe balance. But WooCommerce never found out. Orders sat on "Pending payment" indefinitely. No confirmation email. No stock reduction. No fulfilment trigger.

Over the weekend, 23 orders had gone through Stripe without WooCommerce updating their status. The store had been silently losing track of paid orders for three days before anyone noticed.

How Stripe and WooCommerce Actually Talk to Each Other

Before diving into the diagnosis, it helps to understand the mechanism. When a customer pays via Stripe on a WooCommerce store, two things happen in parallel:

  1. The browser redirect — the customer's browser returns to the order-received page, and WooCommerce attempts to confirm the payment synchronously using the Stripe API.
  2. The webhook — Stripe sends a server-to-server POST request to your site's webhook endpoint (typically https://yoursite.com/?wc-api=wc_stripe) with the payment event details.

The webhook is the safety net. If the browser redirect fails — the customer closes the tab, their connection drops, a JavaScript error breaks the return flow — the webhook still fires and WooCommerce processes the order server-side.

When webhooks stop working, that safety net disappears. And on a busy store, enough edge cases hit the redirect path that orders start falling through.

Finding the Failure in the Stripe Dashboard

My first stop was the Stripe Dashboard under Developers > Webhooks. The endpoint for this store showed a wall of red: HTTP 500 responses going back to Friday afternoon.

Stripe's webhook retry logic is aggressive but finite. After the initial failure, it retries with exponential backoff — roughly 1 hour, 2 hours, 4 hours, then increasingly longer intervals up to about 3 days. After that, Stripe marks the endpoint as disabled and stops sending events entirely.

This store's endpoint had been returning 500s since Friday at 16:42. By Sunday evening, Stripe had exhausted its retries and disabled the webhook. Every payment event from that point forward was simply dropped.

I clicked into one of the failed deliveries to see the response body. Stripe logs the first 1KB of the response from your server. It showed:

Webhook Error: The webhook was not signed with the expected signing secret.

That told me exactly where to look.

The Signing Secret Mismatch

Every Stripe webhook endpoint has a signing secret — a string starting with whsec_ — that Stripe uses to sign each webhook payload. WooCommerce's Stripe plugin verifies this signature on every incoming webhook to confirm it genuinely came from Stripe and wasn't tampered with in transit.

If the signing secret configured in WooCommerce doesn't match the one Stripe is using to sign the payloads, every webhook gets rejected with a signature verification error.

I checked WooCommerce > Settings > Payments > Stripe > Webhook Secret. There was a value there. Then I checked the Stripe Dashboard > Developers > Webhooks > endpoint details > Signing Secret. Different value.

What happened? The store had updated the WooCommerce Stripe Gateway plugin from version 8.5 to 9.2 the previous Friday — right around 16:42 when the failures started. Since version 8.6.1, the Stripe plugin automatically configures webhooks when you connect to Stripe. During the update, the plugin had created a new webhook endpoint in Stripe with a new signing secret, but the old endpoint (with the old secret still saved in WooCommerce settings) was the one still receiving events.

The store now had two webhook endpoints registered in Stripe, both pointing to the same URL:

  • The old one (still active, still receiving events, but WooCommerce was rejecting them because the stored secret had been overwritten with the new endpoint's secret)
  • The new one (also receiving events and correctly signed, but WooCommerce was processing both deliveries — the old endpoint's events hit the signature check first and threw 500 errors, while the new endpoint's events were silently deduplicated or lost in the noise)

This is a subtle failure mode. The plugin update didn't break anything visibly. The checkout still worked for most customers because the synchronous redirect path handled the payment confirmation. But the webhook safety net was gone.

Confirming the Diagnosis

I verified the mismatch from the command line by checking the WooCommerce option where the Stripe plugin stores its webhook secret:

wp option get woocommerce_stripe_settings --format=json | python3 -c "
import sys, json
settings = json.load(sys.stdin)
secret = settings.get('webhook_secret', 'NOT SET')
print(f'Stored webhook secret: {secret[:12]}...')
"
Stored webhook secret: whsec_abc123...

The Stripe Dashboard showed the old endpoint was using whsec_xyz789... — a completely different secret. The stored value matched the new endpoint's secret, not the old one that was actually receiving events.

I also confirmed the failure in the WooCommerce logs:

ls -lt wp-content/uploads/wc-logs/woocommerce-gateway-stripe-* | head -5
grep -c "signing secret" wp-content/uploads/wc-logs/woocommerce-gateway-stripe-*.log

Over 340 signature verification failures logged since Friday afternoon.

The Fix

Three steps to restore webhook delivery:

1. Delete the duplicate endpoint in Stripe. In the Stripe Dashboard under Developers > Webhooks, I removed the old endpoint that was receiving events but being rejected. This left only the new auto-configured endpoint with the correct signing secret.

2. Verify the remaining endpoint works. Stripe lets you send test events from the Dashboard. I clicked "Send test event" and selected payment_intent.succeeded. WooCommerce returned HTTP 200 and the test appeared in the WooCommerce Stripe logs as successfully processed.

3. Reconcile the 23 missing orders. This was the critical step. The orders existed in WooCommerce as "Pending payment" but the money was already in Stripe. I needed to match them up without double-charging anyone.

I pulled the list of pending orders from WooCommerce and cross-referenced against Stripe's successful payments:

wp wc shop_order list --status=pending --after=2026-06-13 \
  --fields=id,total,date_created,billing.email \
  --format=table --user=1

For each pending order, I verified the corresponding charge existed in Stripe by checking the order's _stripe_intent_id meta. On stores using HPOS, wp post meta get won't find order metadata — it lives in the wc_orders_meta table, not wp_postmeta. The safe approach is to use the WooCommerce CLI, which respects whichever storage backend is active:

wp wc shop_order get <order_id> --fields=meta_data --format=json --user=1 \
  | python3 -c "
import sys, json
meta = json.load(sys.stdin).get('meta_data', [])
intent = next((m['value'] for m in meta if m['key'] == '_stripe_intent_id'), None)
print(intent or 'NOT FOUND')
"

Having a _stripe_intent_id on the order doesn't guarantee the payment succeeded — a pending or failed PaymentIntent also gets stored during checkout. Before updating any order, I confirmed each PaymentIntent's status via the Stripe CLI:

stripe payment_intents retrieve <intent_id> --format=json | python3 -c "
import sys, json
pi = json.load(sys.stdin)
print(f'Status: {pi[\"status\"]}  Amount: {pi[\"amount\"]/100:.2f} {pi[\"currency\"].upper()}')
"

Only orders whose PaymentIntent showed succeeded were safe to mark as processing.

Then I updated each confirmed order's status:

wp wc shop_order update <order_id> --status=processing --user=1

This triggered the standard WooCommerce status transition hooks — confirmation emails went out, stock was reduced, and any connected fulfilment integrations picked up the orders.

For the 23 orders, I scripted the reconciliation with a Stripe verification step:

wp wc shop_order list --status=pending --after=2026-06-13 \
  --fields=id --format=ids --user=1 | tr ' ' '\n' | while read order_id; do
  intent_id=$(wp wc shop_order get "$order_id" --fields=meta_data --format=json --user=1 2>/dev/null \
    | python3 -c "
import sys, json
meta = json.load(sys.stdin).get('meta_data', [])
print(next((m['value'] for m in meta if m['key'] == '_stripe_intent_id'), ''))
" 2>/dev/null)
  if [ -z "$intent_id" ]; then
    echo "Order $order_id has no Stripe intent — skipping"
    continue
  fi
  status=$(stripe payment_intents retrieve "$intent_id" --format=json 2>/dev/null \
    | python3 -c "import sys,json; print(json.load(sys.stdin)['status'])" 2>/dev/null)
  if [ "$status" = "succeeded" ]; then
    echo "Order $order_id — intent $intent_id confirmed succeeded — marking as processing"
    wp wc shop_order update "$order_id" --status=processing --user=1
  else
    echo "Order $order_id — intent $intent_id status is '$status' — skipping"
  fi
done

All 23 orders had PaymentIntents with succeeded status. Every one was updated to "processing" and confirmation emails were sent. The store owner reached out to the affected customers with an apology and expedited their shipments.

Why This Breaks More Often Than You'd Expect

I've now seen this same failure pattern — webhook signing secret mismatch after a plugin update — on four different stores. The causes vary slightly:

  • Plugin auto-update changed the webhook configuration without the store owner realising
  • Reconnecting the Stripe account (sometimes prompted after a plugin update) created a new endpoint while leaving the old one active
  • Staging-to-production sync overwrote the live webhook secret with the staging one
  • Multiple people managing the store — one person updated the plugin, another had previously configured webhooks manually, neither knew about the other's setup

The failure is particularly insidious because the primary checkout flow keeps working. Customers pay, see a confirmation page, and assume everything is fine. The store owner sees money arriving in Stripe and assumes the same. It's only when someone checks order statuses — or a customer complains about not receiving a shipping notification — that the gap becomes visible.

Prevention

After fixing this, I added three safeguards to this store's monitoring:

1. Stripe webhook health check via the API. I added a daily cron job that queries the Stripe API for the webhook endpoint's status and recent delivery success rate:

stripe webhook_endpoints list --limit 5 --format=json \
  | python3 -c "
import sys, json
for ep in json.load(sys.stdin).get('data', []):
    print(f'{ep[\"url\"]}  status={ep[\"status\"]}')
"

If the endpoint status isn't enabled or the recent failure rate exceeds 5%, the script sends an alert. On stores where I don't have the Stripe CLI installed, I check the WooCommerce logs instead:

find wp-content/uploads/wc-logs/ -name "woocommerce-gateway-stripe-*" \
  -mtime -1 -exec grep -l "signing secret\|signature" {} \;

Any matches in the last 24 hours mean webhooks are failing silently.

2. Pending order age alert. I set up a WP-CLI check that flags any WooCommerce order that's been in "Pending payment" status for more than 2 hours:

wp db query "
  SELECT p.ID, p.post_date, pm.meta_value as stripe_intent
  FROM $(wp db prefix)posts p
  LEFT JOIN $(wp db prefix)postmeta pm ON p.ID = pm.post_id
    AND pm.meta_key = '_stripe_intent_id'
  WHERE p.post_type = 'shop_order'
    AND p.post_status = 'wc-pending'
    AND p.post_date < DATE_SUB(NOW(), INTERVAL 2 HOUR)
    AND pm.meta_value IS NOT NULL
  ORDER BY p.post_date DESC;
" --skip-column-names

If this returns any rows, those are orders where Stripe has a payment intent but WooCommerce never moved past pending — a strong signal that webhooks aren't being processed.

For stores using HPOS (High-Performance Order Storage), the query targets the wc_orders table instead:

wp db query "
  SELECT o.id, o.date_created_gmt, om.meta_value as stripe_intent
  FROM $(wp db prefix)wc_orders o
  LEFT JOIN $(wp db prefix)wc_orders_meta om ON o.id = om.order_id
    AND om.meta_key = '_stripe_intent_id'
  WHERE o.type = 'shop_order'
    AND o.status = 'wc-pending'
    AND o.date_created_gmt < DATE_SUB(NOW(), INTERVAL 2 HOUR)
    AND om.meta_value IS NOT NULL
  ORDER BY o.date_created_gmt DESC;
" --skip-column-names

3. Post-update webhook verification. After any Stripe plugin update, I manually check the webhook configuration before closing the maintenance ticket. Two endpoints in Stripe for the same site URL is always a problem. One endpoint per store, correct signing secret, correct event subscriptions — verify all three.

The Events Your Webhook Must Subscribe To

While I was auditing this store's webhook, I also checked the event subscriptions. The WooCommerce Stripe plugin needs these webhook events at minimum:

  • payment_intent.succeeded
  • payment_intent.payment_failed
  • payment_intent.requires_action
  • charge.refunded
  • charge.dispute.created
  • charge.dispute.closed
  • review.opened
  • review.closed
  • checkout.session.completed (if using Stripe Checkout)
  • checkout.session.async_payment_succeeded
  • checkout.session.async_payment_failed
  • checkout.session.expired

If you're using Stripe for subscriptions via WooCommerce Subscriptions, you'll also need invoice.payment_succeeded, invoice.payment_failed, and customer.subscription.deleted.

Missing event subscriptions don't cause 500 errors — they cause silent gaps. The webhook endpoint works, but certain payment scenarios just never trigger an update in WooCommerce.

The Takeaway

Stripe webhooks are the backbone of reliable payment processing in WooCommerce. When they break, the failure is silent — no error on the checkout page, no alert in the WordPress dashboard, no email to the store owner. You find out when a customer complains, or when you notice a cluster of "Pending payment" orders that should have been processed days ago.

Every WooCommerce store running Stripe should have monitoring on webhook delivery health. Check the Stripe Dashboard weekly at minimum. Better yet, automate it.


I manage payment gateway configurations and webhook monitoring across the 70+ WordPress sites in my care. If you'd rather not discover missing orders from customer complaints, take a look at my maintenance plans — or read more about how I handle WooCommerce maintenance and database performance for stores that can't afford silent failures.

Stop Firefighting. Start Maintaining.

I manage 70+ WordPress sites for agencies and businesses. Whether you need ongoing maintenance, emergency support, or a one-off performance fix — I can help.

View Maintenance Plans Get in Touch

Get in Touch to Discuss Your Needs