/ Article — WooCommerce Webhooks

WooCommerce Webhooks with Action Scheduler: Reliable Setup

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'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

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