WooCommerce / Integrations
WooCommerce Webhook Troubleshooting
Short answer
Check the webhook's status and its delivery logs first. WooCommerce delivers webhooks in the background through Action Scheduler and disables a webhook after repeated consecutive failed deliveries — any response outside 2xx, or a timeout, counts as a failure. Late deliveries point to Action Scheduler or WP-Cron; failures point to the receiving endpoint (errors, slowness, signature checks, firewalls); duplicates point to receivers that are not idempotent.
Symptoms
- The receiving system never gets some or all events.
- Events arrive minutes or hours late.
- The webhook's status changed to Disabled without anyone touching it.
- The receiver processes the same order twice.
- The receiver rejects deliveries as unauthenticated.
Most common causes
- The receiver fails: it returns 4xx/5xx because of its own bugs, authentication rules or validation of unexpected payloads.
- The receiver is too slow: it does all its work before answering, so the request times out.
- Something blocks the delivery: the receiver's firewall, WAF or IP allowlist; or the store's host blocking outgoing requests.
- Signature verification is wrong: computed over parsed JSON instead of the raw body, or with an old secret.
- Background processing is stuck: Action Scheduler or WP-Cron is not running, so deliveries queue up.
- Receivers are not idempotent:
order.updatedfires on many saves, and retries resend events, so the same order arrives several times.
Diagnosis
1. Check status and delivery logs
Under WooCommerce → Settings → Advanced → Webhooks, check the status (Active, Paused, Disabled), the topic and the delivery URL. Then check WooCommerce → Status → Logs for the webhook delivery log, which records each delivery with its response code and duration.
| What the log shows | Meaning |
|---|---|
| No deliveries for recent events | Event not triggered, or deliveries still queued |
2xx |
Delivered; the problem is on the receiving side |
3xx |
The delivery URL redirects; use the final URL |
401 / 403 |
The receiver or its firewall rejects the request |
5xx |
The receiver failed while processing |
| Timeout / connection error | The receiver is too slow or unreachable |
2. Check the queue
Deliveries run as woocommerce_deliver_webhook_async actions. In Tools → Scheduled Actions, search for that hook: many Pending or past-due actions mean the queue is not being processed; Failed actions contain the error.
3. Check the receiver
On the receiving side, log the raw request, the headers and the response it returns for one test delivery. WooCommerce sends useful headers: X-WC-Webhook-Topic, X-WC-Webhook-Resource, X-WC-Webhook-ID, X-WC-Webhook-Delivery-ID and X-WC-Webhook-Signature.
Logs and technical checks
- Webhook settings: status, topic, delivery URL, secret, API version.
- WooCommerce logs for webhook deliveries.
- Tools → Scheduled Actions: pending, past-due and failed delivery actions.
- Receiver logs: raw body, headers, response code, processing time.
- Firewalls on both sides: outgoing requests from the store, incoming at the receiver.
Solutions
- Answer fast, process later: the receiver should verify the signature, store the event and return 200 immediately, then process it in its own queue.
- Verify signatures over the raw body:
$payload = file_get_contents( 'php://input' );
$signature = $_SERVER['HTTP_X_WC_WEBHOOK_SIGNATURE'] ?? '';
$expected = base64_encode( hash_hmac( 'sha256', $payload, WEBHOOK_SECRET, true ) );
if ( ! hash_equals( $expected, $signature ) ) {
http_response_code( 401 );
exit;
}
// Store the event, return 200 now, process it asynchronously.- Accept the ping that WooCommerce sends when a webhook is saved (form data with
webhook_id) and return 200. - Make processing idempotent: use the delivery id and the resource id plus its modification time to skip events you have already applied.
- Keep the queue moving: make sure WP-Cron runs — preferably from a real server cron job — and clear any Action Scheduler backlog.
- Re-enable the webhook after fixing the receiver: set its status back to Active.
What not to do
- Do not skip signature verification on the receiver; anyone could post fake orders to it.
- Do not run slow work — ERP calls, emails, PDF generation — before answering the webhook.
- Do not re-enable a disabled webhook repeatedly without fixing the receiver; it will be disabled again.
- Do not delete pending delivery actions to "clean up"; they are events the receiver has not seen.
When to contact an expert
Bring in an engineer when the integration loses or duplicates orders, when you control only one side of the integration and need evidence for the other, or when the receiver needs to be rebuilt to be fast, verified and idempotent.
Related problems
- WooCommerce / Performance WooCommerce Action Scheduler Problems Past-due, failed and stuck scheduled actions: how the queue runs, why it stalls and how to fix it without losing pending work.
- WooCommerce / Integrations WooCommerce REST API Troubleshooting Read the status code and the error code in the response: authentication, permissions, routing, security layers and server errors each fail differently.
- WooCommerce / Payments WooCommerce Payment Failed but Customer Was Charged When the gateway captures the payment but the order stays pending or failed, look at the callback and webhook handoff before touching the order.
Frequently asked questions
Why was my WooCommerce webhook disabled?
WooCommerce disables a webhook after a number of consecutive failed deliveries. A delivery fails when the receiver responds outside the 2xx range or does not respond in time. Fix the receiver, then set the webhook back to Active.
Why do webhooks arrive late?
Deliveries are queued as background actions. If WP-Cron or the Action Scheduler runner is not processing the queue — low traffic, a disabled WP-Cron without a real cron job, or a backlog — deliveries wait.
How do I verify a webhook's signature?
Compute a base64-encoded HMAC-SHA256 of the raw request body using the webhook's secret, and compare it with the X-WC-Webhook-Signature header using a constant-time comparison.
Why does my receiver get a request that is not JSON?
When a webhook is saved, WooCommerce sends a ping containing only webhook_id as form data. Receivers should accept it and answer 200, otherwise the first delivery already counts as a failure.