WP Webhooks / Blog / Architecture
Article · Architecture

WooCommerce Webhooks with Action Scheduler: Reliable Setup

Send WooCommerce order events as reliable webhooks via Action Scheduler. Covers hook binding, payload mapping, conditional dispatch, retry logic, and replay.

8 min 2026-05-24
#woocommerce#wordpress

TL;DR

  • WooCommerce's built-in webhook auto-disables after repeated failures, locks you into a fixed payload format, and has no conditional dispatch or replay
  • Webhook Actions queues every delivery via Action Scheduler, returning control to checkout immediately
  • 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 bulk-replayed 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: 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.

/ 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:

PHP — key WooCommerce order hooks

// New order created at checkout
add_action( 'woocommerce_checkout_order_created', function( $order ) {} );

// Payment confirmed (works for all payment methods)
add_action( 'woocommerce_payment_complete', function( $order_id ) {} );

// 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), 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.

/ Queue Delivery

How does Action Scheduler queue WooCommerce webhook deliveries?

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

/ Payload Mapping

How do you map WooCommerce order data to a webhook payload?

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.

PHP — enrich WooCommerce webhook payload

add_filter(
    'fswa_webhook_payload',
    function( $payload, $webhook_id, $trigger ) {
        if ( $trigger !== 'woocommerce_payment_complete' ) {
            return $payload;
        }
        $order_id = $payload[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, 3
);

/ 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. 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.

PHP — skip webhooks for virtual / free orders

add_filter(
    'fswa_should_dispatch',
    function( $should, $webhook_id, $trigger, $payload ) {
        if ( $trigger !== 'woocommerce_payment_complete' ) {
            return $should;
        }
        $order_id = $payload[0] ?? null;
        $order    = wc_get_order( $order_id );
        // Skip free orders (total = 0)
        if ( $order && (float) $order->get_total() === 0.0 ) {
            return false;
        }
        return $should;
    },
    10, 4
);

/ 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 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.

/ Comparison

WooCommerce built-in webhooks vs Webhook Actions: key differences

FeatureWooCommerce Built-in WebhooksWebhook Actions + Action Scheduler
Delivery timingAsync via Action Scheduler — but delivery schedule and retry window are managed by WooCommerce core, not configurableAsync via Action Scheduler — configurable retry schedule, max attempts, and job grouping
Retry logicRepeated failures → webhook auto-disabled by WooCommerce5 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 (free: 1 condition, Pro: unlimited) + fswa_should_dispatch filter for advanced logic
ConcurrencyNo batching — each delivery is independentQueue runner with configurable concurrent batches
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 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.
Why does WooCommerce's built-in webhook auto-disable after failures? +
WooCommerce tracks consecutive delivery failures and disables the webhook after a configurable number of failures (default: 5) to prevent persistent retries against a dead endpoint. This protects server resources but means any extended outage on the receiving side permanently stops delivery. Action Scheduler-backed delivery does not auto-disable — it retries up to the configured max_attempts and then marks individual deliveries permanently_failed, leaving the webhook active for future events.
How do I replay 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 Replay action. Each selected delivery is re-enqueued via Action Scheduler 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. The Pro plan adds Code Glue snippets, which let you attach per-webhook payload transformations directly in the admin UI without filter code.
Does Action Scheduler add overhead to WooCommerce checkout? +
Minimal. The checkout request calls 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.
Ready

Stop losing webhooks.
Start logging them.

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