/ Example — WooCommerce + Claude Code

Ask Claude Code to Set Up a WooCommerce Webhook — It Figures Out the API

You describe what you want. Claude Code hits the REST API, reads the live payload structure, sets a condition — and catches its own field-path mistake before it ships.

/ Prerequisites

What You Need Before Starting

This example uses Webhook Actions by Flow Systems — a free WordPress plugin that delivers webhooks from any WordPress hook, with retry, logging, conditions, and a full REST API.

  1. 1
    Webhook Actions plugin — installed and active

    Search for FlowSystems in Plugins → Add Plugin. One result. Install and activate.

  2. 2
    WooCommerce active with at least one completed order

    The example uses a captured payload from a real order. If you don't have one yet, manually mark any test order as Completed after the webhook is created — the plugin will capture the payload on first fire.

  3. 3
    API token with full scope

    Go to WordPress Admin → Webhook Actions → API Tokens → Add Token. Select scope full (needed to create webhooks and update schemas). Copy the token — it's shown only once.

    set token as shell variable
    export TOKEN="fswa_..." export SITE="https://your-wordpress-site.com"
/ The Session

Claude Code Sets It Up,
Step by Step

This is a real session. Claude Code ran these exact curl commands, got these exact responses, and caught its own mistake before we called it done. Every code block below is from the live API.

/ You said

"I have the Webhook Actions plugin installed on $SITE. The API docs are at wpwebhooks.org/webhook-wordpress-plugin-api/ and my token is in $TOKEN."

Create a webhook that fires when a WooCommerce order status switches to completed (woocommerce_order_status_completed) and sends order data to https://n8n.example.com/webhook/abc12345-demo-0000-0000-example00001 with auth header Bearer your-secret-token. Add a condition: only send if order total is more than 999.

Ohh, one more thing! Please don't spend too much tokens. We have already used 90% of daily limit, and still we need to build that Instagram-like app for agents. Maybe without UI - this needs to be considered though."

Two steps: create the webhook, then configure the condition via the schema endpoint. Starting with the webhook.

step 1 — create the webhook
curl -sk -X POST "$SITE/wp-json/fswa/v1/webhooks" \ -H "X-FSWA-Token: $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "WooCommerce completed → n8n", "endpoint_url": "https://n8n.example.com/webhook/abc12345-demo-0000-0000-example00001", "auth_header": "Bearer your-secret-token", "is_enabled": true, "triggers": ["woocommerce_order_status_completed"] }'
response — 200 OK
{ "id": "25", "name": "WooCommerce completed → n8n", "endpoint_url": "https://n8n.example.com/webhook/abc12345-demo-0000-0000-example00001", "auth_header": "Bearer your-secret-token", "is_enabled": true, "triggers": ["woocommerce_order_status_completed"], "conditions": null, "created_at": "2026-04-29 17:31:14" }

Webhook ID 25 created and enabled. Now the condition. The hook woocommerce_order_status_completed passes the order ID as its first argument — so I'll target args.0.total for the order total.

step 2 — add the condition (first attempt)
curl -sk -X PUT \ "$SITE/wp-json/fswa/v1/schemas/webhook/25/trigger/woocommerce_order_status_completed" \ -H "X-FSWA-Token: $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "conditions": { "enabled": true, "type": "and", "rules": [{ "field": "args.0.total", "operator": "greater_than", "value": "999", "cast": "number" }] } }'
response — 200 OK
{ "id": "15", "webhook_id": "25", "trigger_name": "woocommerce_order_status_completed", "conditions": { "enabled": true, "type": "and", "rules": [{ "field": "args.0.total", "operator": "greater_than", "value": "999", "cast": "number" }] } }

Wait. The API accepted this without complaint — but it doesn't validate field paths. A path is stored as-is and evaluated at runtime when a real order fires. Before declaring this done, let me verify the path is actually correct by pulling the captured payload from another webhook already firing on this same trigger.

step 3 — check the payload structure from an existing webhook
# Webhook 24 already fires on woocommerce_order_status_completed # and has a captured example payload we can inspect curl -sk \ "$SITE/wp-json/fswa/v1/schemas/webhook/24/trigger/woocommerce_order_status_completed" \ -H "X-FSWA-Token: $TOKEN"
response — example_payload (relevant excerpt)
{ "example_payload": { "hook": "woocommerce_order_status_completed", "args": [ 27, ← args[0]: the order ID integer, not the order object { "__type": "Automattic\\WooCommerce\\Admin\\Overrides\\Order", "id": 27, "status": "completed", "total": "1000.00", ← args[1].total is the order total "currency": "USD" ... (full order object) }, { "from": "on-hold", "to": "completed", "manual": true ← args[2]: status transition metadata } ] } }

Found it. args[0] is 27 — a plain integer. That's the raw $order_id WooCommerce passes to the hook. You can't traverse .total on an integer — the condition would silently never match. The serialized order object is at args[1]. Fixing the path now.

step 4 — fix the condition (correct field path)
curl -sk -X PUT \ "$SITE/wp-json/fswa/v1/schemas/webhook/25/trigger/woocommerce_order_status_completed" \ -H "X-FSWA-Token: $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "conditions": { "enabled": true, "type": "and", "rules": [{ "field": "args.1.total", "operator": "greater_than", "value": "999", "cast": "number" }] } }'
response — 200 OK
{ "id": "15", "webhook_id": "25", "conditions": { "enabled": true, "type": "and", "rules": [{ "field": "args.1.total", "operator": "greater_than", "value": "999", "cast": "number" }] }, "updated_at": "2026-04-29 17:34:45" }

Done. Webhook 25 is live. Every time a WooCommerce order is marked completed, the plugin evaluates the condition — if args.1.total exceeds 999 as a number, it queues a delivery to your n8n endpoint with Authorization: Bearer your-secret-token. Orders under 999 are logged as skipped.

Log Details modal showing a skipped delivery — condition not met: args.1.total greater_than 999 (actual: 500.00)
The plugin logs skipped deliveries with the exact condition that failed — here, args.1.total was 500.00, below the 999 threshold.
/ Payload

What Arrives at n8n

Here is the full payload your n8n webhook receives — captured from a real order on this site. The order total is 1000.00, just above the 999 threshold, so this event would pass the condition and be delivered.

Access order fields in n8n via body.args[1].total, body.args[1].billing.email, etc.

POST body — application/json
{ "event": { "id": "3d22cae3-fdd7-467e-8c70-08618a96fc65", "timestamp": "2026-04-27T20:39:11Z", "version": "1.0" }, "hook": "woocommerce_order_status_completed", "args": [ 27, { "__type": "Automattic\\WooCommerce\\Admin\\Overrides\\Order", "id": 27, "status": "completed", "total": "1000.00", "currency": "USD", "customer_id": 1, "payment_method": "bacs", "payment_method_title": "Direct bank transfer", "billing": { "first_name": "Jane", "last_name": "Smith", "email": "[email protected]", "phone": "+1 555 010 0200", "address_1": "123 Main Street", "city": "Austin", "postcode": "73301", "country": "US" }, "line_items": { "1": { "name": "Shampoo Fa", "quantity": 2, "total": "1000", "product_id": 26 } }, "date_created": "2026-04-27T20:31:43+00:00", "date_completed": "2026-04-27T20:39:11+00:00" }, { "from": "on-hold", "to": "completed", "manual": true } ], "timestamp": 1777322351, "site": { "url": "https://your-wordpress-site.com" } }
/ Conditions

Why cast: "number" Matters

WooCommerce stores the order total as a string: "1000.00", not the number 1000.0. Without cast: "number", the greater_than operator would compare strings — and string comparison is alphabetical, not numeric. "999" would be alphabetically greater than "1000.00" because "9" > "1", so all four-digit orders would silently fail the condition.

With cast: "number", the plugin converts the field value to a float before comparison. 1000.0 > 999 — correct.

The condition field path uses dot-notation into the delivery payload. For nested values, keep traversing: args.1.billing.country for the billing country, args.1.line_items.1.quantity for the first line item quantity. If a path segment doesn't exist in the payload, the condition evaluates to false and the delivery is skipped.

How to find any field path: read GET /schemas/webhook/{id}/trigger/{trigger} after the first real event fires. The example_payload field in the response shows the exact structure — traverse it with dot-notation to build your condition.

/ FAQ

Common Questions

The API stores whatever path you give it without validating against a schema. The path is evaluated at runtime — when a real order fires the hook, the plugin resolves args.0.total against the actual payload. args[0] is an integer, so the traversal returns nothing, the comparison fails, and the delivery is silently skipped. No error is logged — the event just never fires. That's why verifying against a captured example payload matters before you ship.
WooCommerce stores order totals as strings — "1000.00" rather than 1000.0. cast: "number" converts the field value to a float before the greater_than comparison runs. Without it, you'd be comparing strings, which is alphabetical: "9" > "1", so "999" would rank higher than "1000.00" and most orders over 999 would fail the condition.
Create a test webhook on that trigger with no conditions, let a real event fire it (or trigger one manually in WooCommerce admin), then read: GET /schemas/webhook/{id}/trigger/{trigger}. The example_payload field in the response shows the exact structure the plugin captured. Use dot-notation to navigate into it for your condition field path.
They are logged as skipped at the condition evaluation step. No delivery attempt is made and no queue job is added, but a log entry is created with a skipped status. Only orders where args.1.total exceeds 999 (as a number) trigger a delivery.
On the free tier you can have one simple rule with AND logic. The Pro plan supports unlimited rules and nested AND/OR groups — so you can combine conditions like args.1.total > 999 AND args.1.billing.country equals "DE" AND args.1.payment_method not_equals "cod".