An order comes in, and the webhook you configured never reaches your app. No error on the store, no error in your logs — WooCommerce webhooks fail quietly, in three specific ways: delivery is deferred to a cron queue that may not be running, the webhook gets silently disabled after 5 consecutive failures, and the activation ping demands a literal HTTP 200. Here's how to diagnose each one, fastest check first.
Point the webhook at a neutral capture URL so your own endpoint is out of the loop.
Create a bin (button below, or curl https://hookden.pages.dev/new), then in
WooCommerce → Settings → Advanced → Webhooks set it as the Delivery URL
and place a test order.
Bins respond 200 by default, so the activation ping (step 5) passes and
you'll see every delivery with full headers — X-WC-Webhook-Topic,
X-WC-Webhook-Resource, X-WC-Webhook-Event,
X-WC-Webhook-Signature, X-WC-Webhook-Delivery-ID — plus the
exact JSON body your handler must parse.
Since WooCommerce 3.5.0, webhooks are delivered asynchronously through Action Scheduler, which runs on WP-Cron — and WP-Cron only runs when someone visits the site. On a quiet staging store, a webhook can sit queued for minutes (or until the next visit) after the order that triggered it.
woocommerce_deliver_webhook_async. Pending entries = deliveries waiting
for cron.define('DISABLE_WP_CRON', true); to wp-config.php and hit
wp-cron.php from a system cron every minute.add_filter('woocommerce_webhook_deliver_async', '__return_false'); — but
note this makes the customer's checkout request wait on your webhook endpoint, so don't
ship it to production casually.WooCommerce counts any response that isn't a 2xx, 301, or 302 as a failed delivery — a 404, a 500, a timeout, all count. After 5 consecutive failures it sets the webhook's status to disabled and stops trying. There is no email, no admin notice, and core WooCommerce does not retry a failed delivery — each failure is simply counted.
woocommerce_max_webhook_delivery_failures filter, but that treats the
symptom; if your endpoint 500s, you want to see why (capture the payload, step 1, and
replay it against your handler).webhooks-delivery source.WooCommerce signs the payload with HMAC-SHA256(secret, raw body),
base64-encoded, in X-WC-Webhook-Signature. Two classic
mistakes:
To check your math against reality: open a captured delivery in the bin, paste your secret, and the signature badge shows ✓/✗ against the raw bytes WooCommerce actually sent — before you touch your own code. Verifying against re-serialized JSON instead of the raw request body is the same bug that bites Stripe and Shopify integrations.
When you activate a webhook, WooCommerce sends a ping (body
webhook_id=…) to the Delivery URL and requires the response code to be
exactly 200 — a 204, a redirect, or an endpoint that validates the body
and returns 4xx will block activation with "Error: Delivery URL cannot be
reached". Make sure your endpoint returns a plain 200 for the ping (or activate
against a bin first to confirm the store side works, then switch the URL).
A live store can't deliver to http://localhost:3000, and the activation
ping will fail before you even get that far. Instead of a tunnel, give WooCommerce a
stable capture URL and relay deliveries down to your machine:
$ curl -s https://hookden.pages.dev/cli -o hookden && chmod +x hookden $ curl https://hookden.pages.dev/new $ ./hookden relay YOUR_BIN http://localhost:3000
Bodies are re-delivered byte-identical, so X-WC-Webhook-Signature still
verifies in your real handler. Details:
webhooks to localhost without a tunnel.
No signup needed. Or from your terminal: curl https://hookden.pages.dev/new