WooCommerce's built-in webhook auto-disables after repeated failures and locks you into a fixed payload format with no conditional dispatch or replay. This article covers binding WooCommerce order hooks to a controlled async queue, mapping order data to a custom payload, conditional dispatch, and replay for missed deliveries.
TL;DR
WooCommerce ships with a built-in webhook system backed by Action Scheduler — deliveries are queued asynchronously just like Webhook Actions. The limitations are elsewhere: failed deliveries are retried a fixed number of times before the entire webhook is auto-disabled, the payload is locked to WooCommerce's own JSON schema, there is no per-delivery conditional logic, and bulk replay after an outage requires re-triggering the original WooCommerce events. High-volume stores — Black Friday traffic, bulk imports — frequently lose delivery windows when the downstream system is briefly unavailable and the built-in retry window is exhausted.
Action Scheduler solves the problem by decoupling delivery from the triggering request. The WooCommerce hook fires, a job is queued in the Action Scheduler database, and the queue runner delivers the payload asynchronously with full exponential-backoff retry and a queryable delivery log.
Every major WooCommerce order lifecycle event exposes a WordPress action hook. The most useful for webhook automation are:
WooCommerce's built-in webhook system already subscribes to a subset of these hooks. When using Webhook Actions, you bind the same hooks directly in wp-admin without writing PHP — the plugin handles the listener registration and queuing.
In Webhook Actions, open wp-admin → Webhook Actions → Add New. Set the Trigger to the WooCommerce hook name (e.g. woocommerce_payment_complete) and the Endpoint URL to your receiving system. The plugin registers the WordPress action listener automatically — no PHP required.
For woocommerce_order_status_changed (which passes four arguments), set the Args count field to 4 so the plugin captures all arguments. The payload mapper then exposes $order_id, $old_status, $new_status, and the $order object as payload fields.
If you need PHP-level control over the binding — per-environment endpoint URLs, runtime conditions — use the REST API to create the webhook programmatically or the fswa_webhook_url filter to rewrite the endpoint URL per delivery.
When the WooCommerce hook fires, Webhook Actions calls as_enqueue_async_action with the webhook ID and serialized trigger arguments as the action args. The current PHP request returns immediately. In the next queue-runner cycle, the action fires do_action() with those args, the plugin reconstructs the payload, and wp_remote_post delivers it to your endpoint.
The queue runner runs outside the WooCommerce checkout request. A 30-second API timeout on your CRM side will not slow down the customer's order confirmation — it will only delay retry processing in the background.
WooCommerce checkout → Webhook Actions → Action Scheduler
Customer WordPress / WooCommerce Queue Runner
│ │ │
│── POST /checkout ───────>│ │
│ │─ woocommerce_payment_complete │
│ │─ as_enqueue_async_action ───> DB (pending)
│<─── Order confirmed ─────│ │
│ │ ┌─── pulls batch │
│ │ │ wp_remote_post(CRM)
│ │ │ 5xx → retry w/ backoff
│ │ └─── log attempt
Webhook Actions automatically serializes the hook arguments into the base payload. For woocommerce_payment_complete, the payload contains $order_id as the root value. For richer payloads — order total, line items, billing email — use the fswa_webhook_payload filter to enrich the outgoing payload just before dispatch.
Webhook Actions has a built-in condition builder in the admin UI — no PHP required. On the free plan you can attach one condition per webhook (e.g. trigger only when a specific post meta value matches). The Pro plan removes that limit, allowing multiple conditions per webhook combined with AND/OR logic.
For conditions that go beyond what the UI supports — checking order totals, product IDs, or any WooCommerce object property — use the fswa_should_dispatch filter. It fires before a job is queued, so returning false keeps the queue clean rather than delivering and discarding on the receiving end.
When a delivery returns a 5xx status or a network error, Webhook Actions marks the attempt failed and schedules a retry via Action Scheduler with exponential backoff: 1 minute, 2 minutes, 4 minutes, 8 minutes — delays capping at 1 hour between attempts. After 5 attempts (configurable via the fswa_max_attempts filter), the delivery is marked permanently_failed.
4xx responses (malformed payload, auth failure) are marked permanently_failed immediately — no retry, because the problem is structural rather than transient.
The delivery log in wp-admin shows every attempt with its HTTP status code and response body. Failed deliveries can be replayed individually or in bulk from the log view — useful when a receiving system undergoes maintenance and you need to re-send a window of missed WooCommerce events without re-triggering the WooCommerce hooks. See the retry and replay architecture for the full backoff schedule and bulk replay workflow.
| Feature | WooCommerce Built-in Webhooks | Webhook Actions + Action Scheduler |
|---|---|---|
| Delivery timing | Async via Action Scheduler — but delivery schedule and retry window are managed by WooCommerce core, not configurable | Async via Action Scheduler — configurable retry schedule, max attempts, and job grouping |
| Retry logic | Repeated failures → webhook auto-disabled by WooCommerce | 5 attempts, exponential backoff, never auto-disabled |
| Delivery log | Basic — logs exist but limited history | Full per-attempt log with status codes and response body |
| Manual replay | No replay — must re-trigger the WooCommerce event | Replay individual or bulk deliveries from admin log |
| Payload control | Fixed WooCommerce payload format | fswa_webhook_payload filter — any shape, any enrichment |
| Conditional dispatch | No — all matching events are delivered | UI condition builder (free: 1 condition, Pro: unlimited) + fswa_should_dispatch filter for advanced logic |
| Concurrency | No batching — each delivery is independent | Queue runner with configurable concurrent batches |
woocommerce_payment_complete fires after a payment is recorded as complete, regardless of payment method. It receives $order_id as its only argument. woocommerce_checkout_order_created fires earlier, when the order row is first created — before payment confirmation.
fswa_webhook_payload filter to shape each payload differently based on $webhook_id. The Pro plan adds Code Glue snippets, which let you attach per-webhook payload transformations directly in the admin UI without filter code.
as_enqueue_async_action, which performs a single DB INSERT into the Action Scheduler tables and returns immediately. All delivery work, including wp_remote_post and retry logic, happens in the queue runner process outside the checkout request. The DB write is the only synchronous cost.