TL;DR
- WooCommerce's built-in webhook makes one attempt per event with no retry, auto-disables after repeated consecutive failures, locks you into a fixed payload format, and has no conditional dispatch or replay
- Webhook Actions writes every delivery to its own queue table and returns control to checkout immediately; a single recurring Action Scheduler job drains that queue every minute
- Use
fswa_webhook_payloadto enrich order data andfswa_should_dispatchto filter by status, total, or product - Failed deliveries retry with exponential backoff and can be retried again in bulk from the admin log without re-triggering WooCommerce hooks
/ Overview
Why do WooCommerce webhooks need a persistent queue?
WooCommerce ships with a built-in webhook system backed by Action Scheduler — deliveries are queued asynchronously just like Webhook Actions. The limitations are elsewhere: each event gets exactly one delivery attempt — a failed delivery is never retried, it only increments the webhook's failure count, and after repeated consecutive failures (the 7th by default) the entire webhook is 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 events when the downstream system is briefly unavailable, because there is no retry to catch them.
A persistent queue solves the problem by decoupling delivery from the triggering request. The WooCommerce hook fires, a job is written to a database queue, and a background runner — driven by Action Scheduler on a WooCommerce site — delivers the payload asynchronously with exponential-backoff retry and a queryable delivery log.
/ WooCommerce Hooks
Which WooCommerce hooks should trigger webhook deliveries?
Every major WooCommerce order lifecycle event exposes a WordPress action hook. The most useful for webhook automation are:
Note that the first of these is checkout-only and never fires on the blocks checkout — see when woocommerce_checkout_order_created actually fires before binding to it.
PHP — key WooCommerce order hooks
// New order created at checkout add_action( 'woocommerce_checkout_order_created', function( $order ) {} ); // Payment confirmed by a gateway via $order->payment_complete(). // BACS, cheque and cash on delivery don't call it for paid orders. add_action( 'woocommerce_payment_complete', function( $order_id, $transaction_id ) {}, 10, 2 ); // Any status transition — from pending to processing, etc. add_action( 'woocommerce_order_status_changed', function( $order_id, $old_status, $new_status, $order ) {}, 10, 4 ); // Specific transition hooks (cleaner for single-status targets) add_action( 'woocommerce_order_status_completed', function( $order_id ) {} ); add_action( 'woocommerce_order_status_refunded', function( $order_id ) {} );
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.
/ Hook Binding
How do you bind a WooCommerce hook to a webhook endpoint?
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) there is nothing to configure: the plugin registers every trigger with no argument limit, so all arguments are captured. 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.
/ Queue Delivery
How does Action Scheduler queue WooCommerce webhook deliveries?
When the WooCommerce hook fires, Webhook Actions builds the payload right away and inserts a job into its own queue table, wp_fswa_queue, with a pending row in its delivery log. It does not create an Action Scheduler action per delivery. Instead it registers one recurring Action Scheduler action, fswa_process_queue, every 60 seconds; each run takes up to 10 due jobs (filter fswa_queue_batch_size) and delivers them with wp_remote_post. Without Action Scheduler, the same runner is a WP-Cron event.
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.
/ Payload Mapping
How do you map WooCommerce order data to a webhook payload?
Webhook Actions automatically serializes the hook arguments into the base payload. Every payload has the same envelope — event, hook, args, timestamp, site — with the hook's arguments in args, so for woocommerce_payment_complete the order ID is args[0]. If you mapped fields in the UI, the payload you receive is the mapped shape and the original envelope arrives as the fourth argument. For richer payloads — order total, line items, billing email — use the fswa_webhook_payload filter to enrich the outgoing payload just before dispatch.
PHP — enrich WooCommerce webhook payload
add_filter( 'fswa_webhook_payload', function( $payload, $webhook_id, $trigger, $original_payload ) { if ( $trigger !== 'woocommerce_payment_complete' ) { return $payload; } // The envelope; when field mapping is on, $payload is the mapped shape. $envelope = $original_payload ?? $payload; $order_id = $envelope['args'][0] ?? null; $order = wc_get_order( $order_id ); if ( ! $order ) return $payload; return [ 'order_id' => $order_id, 'status' => $order->get_status(), 'total' => $order->get_total(), 'currency' => $order->get_currency(), 'billing_email' => $order->get_billing_email(), 'line_items' => array_map( fn( $item ) => [ 'product_id' => $item->get_product_id(), 'name' => $item->get_name(), 'quantity' => $item->get_quantity(), ], $order->get_items() ), ]; }, 10, 4 );
/ Conditional Dispatch
How do you filter WooCommerce webhooks by status or product?
Webhook Actions has a built-in condition builder in the admin UI — no PHP required. As of 3.0.0 there is no cap on the free plan: attach as many conditions per webhook as you need and combine them 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 receives $should_dispatch, the trigger name and the raw hook arguments, and runs once per trigger before any webhook is matched or job queued, so returning false keeps the queue clean rather than delivering and discarding on the receiving end.
PHP — skip webhooks for virtual / free orders
add_filter( 'fswa_should_dispatch', function( $should, $trigger, $args ) { if ( $trigger !== 'woocommerce_payment_complete' ) { return $should; } $order_id = $args[0] ?? null; // raw hook arguments $order = wc_get_order( $order_id ); // Skip free orders (total = 0) if ( $order && (float) $order->get_total() === 0.0 ) { return false; } return $should; }, 10, 3 );
/ Retry & Replay
What retry behavior applies to failed WooCommerce deliveries?
When a delivery returns a 5xx status or a network error, Webhook Actions marks the attempt failed and pushes the job's next run time out in its own queue with exponential backoff: 1 minute, 2 minutes, 4 minutes, 8 minutes — delays capping at 1 hour between attempts. After 5 attempts (configurable per webhook in the UI, or with 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 retried individually or in bulk from the log view (Retry) — 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. Replay is the separate action for re-sending deliveries that already succeeded or were skipped. See the retry and replay architecture for the full backoff schedule and bulk replay workflow.
/ Comparison
WooCommerce built-in webhooks vs Webhook Actions: key differences
| Feature | WooCommerce Built-in Webhooks | Webhook Actions + Action Scheduler |
|---|---|---|
| Delivery timing | Async via Action Scheduler — one attempt per event, no retry | Own queue table drained every minute (Action Scheduler or WP-Cron) — configurable retry limit and backoff per webhook |
| Retry logic | No retry; 7th consecutive failure (default) → webhook auto-disabled | 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 (unlimited AND/OR conditions, free) + fswa_should_dispatch filter for advanced logic |
| Concurrency | No batching — each delivery is independent | Batched queue runs (fswa_queue_batch_size, default 10 per run) |
Seeing it run beats reading about it. The live preview boots a throwaway WordPress with Webhook Actions already installed and demo deliveries sitting in the log — no signup, nothing left on your machine afterwards.