WP Webhooks / Blog / Architecture

WooCommerce Webhooks via Action Scheduler: Retry & Replay

Send WooCommerce order events as reliable webhooks with Action Scheduler — async queue, retries with backoff, conditional dispatch, and one-click replay.

8 min 2026-05-24 Updated 2026-10-14
#woocommerce#wordpress

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_payload to enrich order data and fswa_should_dispatch to 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

FeatureWooCommerce Built-in WebhooksWebhook Actions + Action Scheduler
Delivery timingAsync via Action Scheduler — one attempt per event, no retryOwn queue table drained every minute (Action Scheduler or WP-Cron) — configurable retry limit and backoff per webhook
Retry logicNo retry; 7th consecutive failure (default) → webhook auto-disabled5 attempts, exponential backoff, never auto-disabled
Delivery logBasic — logs exist but limited historyFull per-attempt log with status codes and response body
Manual replayNo replay — must re-trigger the WooCommerce eventReplay individual or bulk deliveries from admin log
Payload controlFixed WooCommerce payload formatfswa_webhook_payload filter — any shape, any enrichment
Conditional dispatchNo — all matching events are deliveredUI condition builder (unlimited AND/OR conditions, free) + fswa_should_dispatch filter for advanced logic
ConcurrencyNo batching — each delivery is independentBatched queue runs (fswa_queue_batch_size, default 10 per run)
try_it

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.

FAQ

Common questions always ask.

Don't see yours? Open an issue on GitHub or check the full reference in the API docs.

Which WooCommerce hook fires when a payment is confirmed? +
woocommerce_payment_complete fires when a payment gateway confirms payment by calling $order->payment_complete(). The core BACS, cheque and cash-on-delivery gateways do not call it for paid orders, and changing an order's status by hand does not fire it. It passes two arguments: $order_id and $transaction_id. woocommerce_checkout_order_created fires earlier, when the order row is first created — before payment confirmation.
Why does WooCommerce's built-in webhook auto-disable after failures? +
WooCommerce makes one delivery attempt per event and never retries it; a failure only increments the webhook's failure count, and a success resets it. Once the count passes the woocommerce_max_webhook_delivery_failures threshold (default 5), the next failure disables the webhook — so by default the 7th consecutive failure turns it off, and any extended outage on the receiving side stops delivery entirely. Webhook Actions does not auto-disable: it retries each delivery up to its configured attempt limit, then marks that delivery permanently_failed and leaves the webhook active for future events.
How do I re-send WooCommerce webhook deliveries that failed during an outage? +
In wp-admin → Webhook Actions → Logs, filter by status = permanently_failed and the date range of the outage. Select all matching entries and use the bulk Retry action (Replay is only for deliveries that already succeeded or were skipped). Each selected delivery is re-queued in Webhook Actions' own delivery queue and retried with the original payload — you do not need to re-trigger the WooCommerce hooks or touch the order data.
Can I send different payloads to different endpoints for the same WooCommerce hook? +
Yes. Create multiple webhooks in Webhook Actions, each pointing to a different endpoint, all bound to the same hook. Use the fswa_webhook_payload filter to shape each payload differently based on $webhook_id. Code Glue snippets, free since 3.0.0, let you attach per-webhook payload transformations directly in the admin UI without filter code.
Does Action Scheduler add overhead to WooCommerce checkout? +
Minimal. During checkout, Webhook Actions builds the payload and writes a job row to its own queue table plus a pending row to its delivery log, then returns. All delivery work, including wp_remote_post and retry logic, happens in the background queue runner — one recurring Action Scheduler action every minute — outside the checkout request. Those DB writes are the only synchronous cost.
Ready

Your next automation is
one sentence away.

$ wp plugin install flowsystems-webhook-actions --activate