---
title: "WooCommerce Webhooks via Action Scheduler: Retry & Replay"
description: "Send WooCommerce order events as reliable webhooks with Action Scheduler — async queue, retries with backoff, conditional dispatch, and one-click replay."
url: "https://wpwebhooks.org/blog/woocommerce-webhooks-action-scheduler/"
date: "2026-05-24"
---

# WooCommerce Webhooks via Action Scheduler: Retry & Replay

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](https://actionscheduler.org/) 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](https://woocommerce.com/document/webhooks/) 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](https://wordpress.org/plugins/flowsystems-webhook-actions/) 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.

/ 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](https://wpwebhooks.org/blog/wordpress-webhook-retry-replay-system/) 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 — 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 |

## Structured data

```json
{"@context":"https://schema.org","@type":"Article","headline":"WooCommerce Webhooks via Action Scheduler: Retry & Replay","description":"Send WooCommerce order events as reliable webhooks with Action Scheduler — async queue, retries with backoff, conditional dispatch, and one-click replay.","datePublished":"2026-05-24","dateModified":"2026-05-24","author":{"@type":"Person","name":"Mateusz Skorupa","url":"https://wpwebhooks.org/about/"},"publisher":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org"},"url":"https://wpwebhooks.org/blog/woocommerce-webhooks-action-scheduler/","image":"https://wpwebhooks.org/og_image.jpg","keywords":["woocommerce webhooks","woocommerce action scheduler","woocommerce webhook delivery","woocommerce order webhook","woocommerce payment complete webhook","woocommerce webhook retry","wordpress woocommerce webhook integration"]}

{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"WP Webhooks","item":"https://wpwebhooks.org/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://wpwebhooks.org/blog/"},{"@type":"ListItem","position":3,"name":"WooCommerce Webhooks via Action Scheduler: Retry & Replay","item":"https://wpwebhooks.org/blog/woocommerce-webhooks-action-scheduler/"}]}

{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Which WooCommerce hook fires when a payment is confirmed?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"Why does WooCommerce's built-in webhook auto-disable after failures?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"How do I replay WooCommerce webhook deliveries that failed during an outage?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"Can I send different payloads to different endpoints for the same WooCommerce hook?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"Does Action Scheduler add overhead to WooCommerce checkout?","acceptedAnswer":{"@type":"Answer","text":"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."}}]}
```
