Platform Spoke · WooCommerce
Spoke · Make Your Platform Agent-Ready

Make Your WooCommerce Store Agent-Ready — Schema, REST API, and Webhooks.

WooCommerce gives you a real REST API at /wp-json/wc/v3/, a 10.3 native MCP integration in developer preview, native global_unique_id for GTIN, and — as of WooCommerce 10.7 — an agentic_commerce payment gateway flag for ACP-style flows. What it does not give you is rate limiting on the v3 REST API, a UCP listing at the January 2026 NRF launch, or a one-click schema block that covers the 20-field MVP. This is a weekend retrofit, not a greenfield build: extend the schema via a wp_head PHP snippet, generate consumer keys, put Cloudflare or nginx in front of the API, enable the native MCP preview or ship a Python wrapper, and wire the ACP/UCP hooks.

4
Layers to Close
Weekend
Retrofit
/wp-json/wc/v3/
REST API Base
Jan 2026
UCP Launched Without Woo
§1 · The Weekend Reality

Four layers. One honest gap. One operational store.

WooCommerce is the most-installed e-commerce platform on the open web and the most flexible — you own the database, the PHP, the host, the cron, the cache layer, and every line of the front-end. That flexibility cuts both ways for agent commerce. Layer 1 — structured data: Yoast and RankMath both ship Product schema, but neither covers the 20-field MVP, and stacking multiple sources creates duplicate Product blocks that break Google validation. You replace them with a single wp_head PHP snippet. Layer 2 — API: the WC v3 REST API at /wp-json/wc/v3/ is real, GraphQL-equivalent in surface, and battle-tested — but it ships no native rate limiting, which is the single biggest operational gap an agent integration has to close before launch. Layer 3 — MCP: WooCommerce 10.3 (late 2025) shipped a native MCP integration as a developer-preview feature, disabled by default, mounted at /wp-json/woocommerce/mcp; for production today most operators ship a custom Python MCP wrapper over the REST API and swap to native when the preview lifts. Layer 4 — UCP: the Universal Commerce Protocol launched at NRF on January 11, 2026 with Google + Shopify; the April 2026 expanded partner list did not include WooCommerce. WooCommerce 10.7 (April 14, 2026) shipped a native agentic_commerce payment gateway flag for ACP-style flows — the closest first-party hook — but you still hand-roll the UCP .well-known manifest yourself. Your weekend job: close all four. Re-verify the 10.3 MCP preview status, 10.7 ACP flag, and Cloudflare plan limits against current release notes before launch.

§2 · The 4-Layer Model on WooCommerce

What you build on top of what WooCommerce already gives you.

The full agent-readiness model is documented on the AgentMall Roadmap. On WooCommerce, two of the four layers ship in the box (REST API, native product fields including global_unique_id) and the other two are partially staged (10.3 native MCP in preview; 10.7 ACP gateway flag). Your weekend is about closing the gaps inside each layer — and naming the operational gaps the platform does not close for you.

Layer 1 · Schema

Structured Data

Yoast and RankMath ship partial Product schema. You replace them with one wp_head PHP snippet covering all 20 MVP fields, including global_unique_id (native WC 8.x+). See Product Data for the field reference.

Layer 2 · API

WC v3 REST API + Store API

WC v3 REST API at /wp-json/wc/v3/ covers eight canonical agent endpoints. No native rate limiting — push it to Cloudflare or nginx. Store API at /wp-json/wc/store/v1/ has optional 25 req/10s default (disabled). See API Endpoint.

Layer 3 · MCP

10.3 Native Preview + Python Wrapper

WooCommerce 10.3 ships a native MCP integration at /wp-json/woocommerce/mcp in developer preview, disabled by default. Enable with one wp-cli option, or ship a custom Python MCP wrapper today for production. See MCP.

Layer 4 · UCP

Hand-Rolled UCP + 10.7 ACP Flag

WooCommerce was not on the Jan 2026 UCP launch list or the April 2026 expanded set. You publish your own /.well-known/ucp manifest. WooCommerce 10.7 adds a native agentic_commerce payment gateway flag for ACP. See UCP.

Why This Order Matters

Schema is the discovery layer — without it, agents cannot find or rank your products. The API is the transaction layer — without it, they cannot buy. MCP is the negotiation layer — without it, the agent has to write your API integration itself. UCP is the standardization layer — without it, every agent platform demands a custom integration. On WooCommerce specifically, the API and Schema layers are mature and self-served; MCP is in active preview; UCP is something you assemble. Build in order, test each one, then test them together.

Layer 1 · Structured Data

Schema.org on WooCommerce: one wp_head snippet, no plugin stack.

WooCommerce core emits a minimal Product microdata block on single-product pages, and most SEO plugins layer their own Product JSON-LD on top. That is the trap. Stacking Yoast WooCommerce SEO, RankMath Pro, and a theme's built-in JSON-LD output produces two or three conflicting Product blocks per page, which Google's Rich Results Test flags as a structured-data error and which most AI shopping agents simply treat as untrusted. The fix is to pick exactly one source of Product schema, disable the others, and make sure the one you keep covers all 20 fields of the AgentMall MVP.

What WooCommerce auto-emits

The Storefront theme and most modern WooCommerce themes emit microdata attributes on .product markup, and the core WC_Structured_Data class injects a JSON-LD Product block in the footer. Yoast WooCommerce SEO (~$178.80/yr bundled — re-verify before launch) and RankMath Pro (~$84/yr unlimited sites — re-verify before launch) each ship their own Product schema generators that override or duplicate this output. The safe production pattern: pick one source, disable the others under each plugin's schema settings, then validate at Google's Rich Results Test.

The 20-field MVP — what to emit

FieldSourceWooCommerce LocationStatus
@typeSchema.orgLiteralSnippet
name$product->get_name()Product editorSnippet
description$product->get_description()Product editorSnippet
imagewp_get_attachment_url()Product gallerySnippet
brandproduct_brand taxonomy or _brand metaBrand plugin / custom fieldPopulate
sku$product->get_sku()Inventory tabSnippet
price$product->get_price()General tabSnippet
priceCurrencyget_woocommerce_currency()SettingsSnippet
availability$product->get_stock_status()Inventory tabSnippet
url$product->get_permalink()ComputedSnippet
gtin / global_unique_id$product->get_global_unique_id()Inventory tab (WC 8.x+)Populate
mpn_mpn custom metaCustom fieldPopulate
aggregateRating.ratingValue$product->get_average_rating()Native reviewsSnippet
aggregateRating.reviewCount$product->get_review_count()Native reviewsSnippet
hasMerchantReturnPolicy.merchantReturnDaysLiteral or _return_policy metaSnippetSnippet
hasMerchantReturnPolicy.returnPolicyCategoryLiteralSnippetSnippet
shippingDetails.shippingLabel$product->get_shipping_class()Shipping tabSnippet
shippingDetails.shippingDestinationLiteralSnippetSnippet
itemCondition_condition metaCustom fieldPopulate
categoryproduct_cat taxonomyCategories tabSnippet

The wp_head PHP snippet — full source

Add this to functions.php in a child theme or, better, drop it into a site-specific plugin so it survives theme changes. It hooks wp_head at priority 99 (after most plugins), reads native WooCommerce fields where possible, falls back to custom meta where WooCommerce does not natively store the field, and loops through variations to emit one Offer per variation on variable products. Disable Yoast and RankMath Product schema output before you ship this, or you will end up with duplicate blocks.

<?php
/**
 * Output agent-ready Schema.org Product JSON-LD on WooCommerce single product pages.
 * Covers all 20 fields of the AgentMall MVP product object.
 * Add to functions.php or a site-specific plugin (not a theme you didn't write).
 */
add_action( 'wp_head', 'agentmall_product_jsonld', 99 );

function agentmall_product_jsonld() {
    if ( ! is_product() ) {
        return;
    }

    global $post;
    $product = wc_get_product( $post->ID );

    if ( ! $product instanceof WC_Product ) {
        return;
    }

    // --- Basic fields ---
    $id           = $product->get_id();
    $name         = $product->get_name();
    $description  = $product->get_description() ?: $product->get_short_description();
    $price        = $product->get_price();
    $currency     = get_woocommerce_currency();
    $sku          = $product->get_sku();
    $permalink    = $product->get_permalink();
    $date_mod     = $product->get_date_modified();
    $last_updated = $date_mod ? $date_mod->date( 'c' ) : '';

    // --- Image ---
    $image_id  = $product->get_image_id();
    $image_url = $image_id ? wp_get_attachment_url( $image_id ) : '';

    // --- Availability ---
    $stock_status = $product->get_stock_status(); // 'instock', 'outofstock', 'onbackorder'
    $availability_map = [
        'instock'     => 'https://schema.org/InStock',
        'outofstock'  => 'https://schema.org/OutOfStock',
        'onbackorder' => 'https://schema.org/BackOrder',
    ];
    $availability = $availability_map[ $stock_status ] ?? 'https://schema.org/OutOfStock';

    // --- Category ---
    $categories   = wp_get_post_terms( $id, 'product_cat', [ 'fields' => 'names' ] );
    $category_str = ! is_wp_error( $categories ) && $categories ? implode( ', ', $categories ) : '';

    // --- Brand (requires 'product_brand' taxonomy from a brand plugin, or fallback to meta) ---
    $brands     = wp_get_post_terms( $id, 'product_brand', [ 'fields' => 'names' ] );
    $brand_name = ( ! is_wp_error( $brands ) && $brands ) ? $brands[0] : get_post_meta( $id, '_brand', true );

    // --- GTIN / UPC / EAN (WooCommerce 8.x+ native field) ---
    $gtin = method_exists( $product, 'get_global_unique_id' ) ? $product->get_global_unique_id() : get_post_meta( $id, '_global_unique_id', true );

    // --- Aggregate Rating ---
    $avg_rating   = (float) $product->get_average_rating();
    $review_count = (int)   $product->get_review_count();

    // --- Custom meta (populate via product editor or CSV import) ---
    $return_policy  = get_post_meta( $id, '_return_policy', true );
    $shipping_class = $product->get_shipping_class();
    $condition_raw  = get_post_meta( $id, '_condition', true ) ?: 'new';
    $condition_map  = [
        'new'         => 'https://schema.org/NewCondition',
        'used'        => 'https://schema.org/UsedCondition',
        'refurbished' => 'https://schema.org/RefurbishedCondition',
    ];
    $condition = $condition_map[ $condition_raw ] ?? 'https://schema.org/NewCondition';

    // --- Variants (variable product) — one Offer per variation ---
    $offers = [];
    if ( $product->is_type( 'variable' ) ) {
        foreach ( $product->get_children() as $variation_id ) {
            $variation = wc_get_product( $variation_id );
            if ( ! $variation || ! $variation->is_purchasable() ) { continue; }
            $var_stock = $variation->get_stock_status();
            $offers[]  = [
                '@type'           => 'Offer',
                'sku'             => $variation->get_sku(),
                'price'           => $variation->get_price(),
                'priceCurrency'   => $currency,
                'availability'    => $availability_map[ $var_stock ] ?? 'https://schema.org/OutOfStock',
                'url'             => $permalink . '?variation_id=' . $variation_id,
                'itemCondition'   => $condition,
                'priceValidUntil' => gmdate( 'Y-12-31' ),
            ];
        }
    } else {
        $offers[] = [
            '@type'           => 'Offer',
            'price'           => $price,
            'priceCurrency'   => $currency,
            'availability'    => $availability,
            'url'             => $permalink,
            'itemCondition'   => $condition,
            'priceValidUntil' => gmdate( 'Y-12-31' ),
        ];
    }

    // --- Build schema array ---
    $schema = [
        '@context'     => 'https://schema.org',
        '@type'        => 'Product',
        '@id'          => $permalink . '#product',
        'productID'    => (string) $id,
        'name'         => $name,
        'description'  => wp_strip_all_tags( $description ),
        'sku'          => $sku,
        'url'          => $permalink,
        'image'        => $image_url,
        'category'     => $category_str,
        'offers'       => $offers,
        'dateModified' => $last_updated,
    ];

    if ( $brand_name ) {
        $schema['brand'] = [ '@type' => 'Brand', 'name' => $brand_name ];
    }
    if ( $gtin ) {
        $schema['gtin'] = $gtin;
    }
    if ( $avg_rating > 0 && $review_count > 0 ) {
        $schema['aggregateRating'] = [
            '@type'       => 'AggregateRating',
            'ratingValue' => $avg_rating,
            'reviewCount' => $review_count,
            'bestRating'  => 5,
            'worstRating' => 1,
        ];
    }
    if ( $return_policy ) {
        $schema['hasMerchantReturnPolicy'] = [
            '@type'                => 'MerchantReturnPolicy',
            'applicableCountry'    => 'US',
            'returnPolicyCategory' => 'https://schema.org/MerchantReturnFiniteReturnWindow',
            'merchantReturnDays'   => 30,
            'returnMethod'         => 'https://schema.org/ReturnByMail',
            'description'          => esc_html( $return_policy ),
        ];
    }
    if ( $shipping_class ) {
        $schema['shippingDetails'] = [
            '@type'         => 'OfferShippingDetails',
            'shippingLabel' => $shipping_class,
            'doesNotShip'   => false,
        ];
    }

    echo '<script type="application/ld+json">' . wp_json_encode( $schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT ) . '</script>' . "\n";
}
Critical · Disable Duplicate Schema Sources

Before deploying this snippet, disable Product schema output in every other plugin that emits it. In Yoast: SEO → Search Appearance → Content Types → Products and turn off Schema. In RankMath: Rank Math → Titles & Meta → WooCommerce and disable schema. If your theme has a built-in JSON-LD toggle, turn that off too. Then re-validate every product template at the Rich Results Test. Duplicate Product blocks are the single most common reason agent shopping platforms exclude a WooCommerce store from results.

Populating global_unique_id (GTIN / UPC / EAN)

WooCommerce 8.x added global_unique_id as a native product field, surfaced in the Inventory tab of the product editor. Before that release, operators stored GTINs in custom meta (_gtin, _upc) — the snippet above falls back to _global_unique_id if the native getter is unavailable. Bulk-populate from a supplier feed with Matrixify (~$59/yr — re-verify before launch) or a one-off WP-CLI script. Without GTIN coverage, Google AI Overviews demotes your products and most agent shopping platforms quietly drop them.

Schema plugins if you do not want to edit functions.php

PluginApproachPrice (re-verify before launch)
Yoast WooCommerce SEOBundles Product schema + sitemaps + meta tags; safe enterprise default~$178.80/yr bundled
RankMath ProMost complete WooCommerce schema; unlimited sites on one license~$84/yr Pro
Schema ProFAQ, HowTo, Article schema in addition to Product — pair with the snippet for non-product pages~$79/yr intro / ~$249 lifetime
All In One SEO ProSchema + meta + sitemap suite; WooCommerce module bundled~$199.50/yr
WPSSO WC Metadata ProGranular WooCommerce schema with per-variation Offers and shipping policy fields~$59.99/yr

For the field-by-field reference and the cross-platform 20-field MVP that this PHP snippet implements, see the Product Data spoke. For Schema-only validation tactics, run every change through the Rich Results Test and Schema.org validator before deploying to production.

Layer 2 · API Endpoint

WC v3 REST API for everything. Rate-limit at the edge.

WooCommerce ships a real REST API at /wp-json/wc/v3/, requires WordPress 4.4+ with pretty permalinks enabled, and covers products, variations, orders, customers, refunds, webhooks, and reports across the full agent flow. There is also a newer Store API at /wp-json/wc/store/v1/ for cart and checkout state, and an older v1/v2 surface marked legacy. For agent integrations the right choices are v3 REST for the canonical eight-endpoint surface and the Store API for browser-driven cart redirects. The single biggest production gap: the WC v3 REST API ships no native rate limiting, so a misbehaving agent (or a malicious one with a valid key) can saturate PHP-FPM and the database. You close this gap at the edge with Cloudflare WAF rules or an nginx limit_req_zone in front of PHP-FPM — and the gap exists by design, not by oversight.

v3 REST vs Store API at a glance

PropertyWC v3 REST APIStore API
Surface/wp-json/wc/v3//wp-json/wc/store/v1/
ProtocolREST (JSON)REST (JSON)
AuthBasic Auth (ck_/cs_) over HTTPS; OAuth 1.0a legacyCookie / Nonce (browser-native); CORS-friendly
Rate limitNone native (push to edge — Cloudflare / nginx)Optional 25 req / 10s default, disabled out of the box
Who uses itBackend services, server-side agents, custom MCP wrappersHeadless / decoupled checkout, theme-side cart UI
Cart vs OrdersPOST /orders to mint orders directly (with set_paid)/cart and /checkout for browser-driven flows
Typical agent uselist_products, get_product, search, check_inventory, calculate_total, initiate_checkout, get_order_status, request_returnHosted cart redirect for human-completed checkout
Critical · The v3 REST API Has No Native Rate Limiting

WooCommerce core does not throttle /wp-json/wc/v3/. The Store API at /wp-json/wc/store/v1/ ships an optional 25 req/10s default that is disabled by default and can be tuned via the woocommerce_store_api_rate_limit_options filter — but it does not cover the v3 surface agents actually call. Push rate limiting to Cloudflare WAF (free plan ships basic rules), an nginx limit_req_zone in front of PHP-FPM, or your managed-host WAF, keyed on the consumer key prefix so a single misbehaving agent is throttled without affecting the rest of the API. This is the single most-skipped step in WooCommerce agent integrations.

Generating consumer key + secret

  1. WooCommerce Admin → Settings → Advanced → REST API → Add Key.
  2. Description: agent-readonly-2026-06 (or similar — rotate on a schedule).
  3. User: a dedicated WordPress user with the minimum capability set (a custom agent_readonly role is the safer pattern; shop_manager is overkill).
  4. Permissions: Read for buyer-facing agents, Read/Write only for server-side automation behind your own proxy.
  5. Click Generate API Key. The consumer secret is shown once — save both immediately to your secrets manager (1Password, Bitwarden, AWS Secrets Manager, Doppler).
  6. Set as WC_CONSUMER_KEY and WC_CONSUMER_SECRET in your environment. Never commit either to source control. Never expose the write-scoped pair to client-side or agent code.

The eight canonical agent endpoints — mapped to WooCommerce

The API spoke defines eight canonical endpoints that cover the full agent purchase flow. WooCommerce implements all eight through the v3 REST API plus a small custom endpoint for total calculation. Map yours from this table.

Canonical EndpointWooCommerce PathMethod · Notes
list_products/wp-json/wc/v3/productsGET — paginate with per_page and page
get_product/wp-json/wc/v3/products/{id}GET — variable products: also fetch /products/{id}/variations
search_products/wp-json/wc/v3/products?search=...GET — supports category, min_price, max_price, stock_status
check_inventory/wp-json/wc/v3/products/{id}GET — read stock_status, stock_quantity, purchasable
calculate_total/wp-json/agentmall/v1/calculate-totalPOST — custom endpoint (see PHP below)
initiate_checkout/wp-json/wc/v3/ordersPOST — full order with line_items, billing, shipping, payment_method, set_paid
get_order_status/wp-json/wc/v3/orders/{id}GET — read status, date_paid, line items, totals
request_return/wp-json/wc/v3/orders/{id}/refundsPOST — server-side only via the write-scoped key

list_products — curl

curl -X GET "https://yourstore.com/wp-json/wc/v3/products?per_page=20&status=publish&stock_status=instock" \
  -u $WC_CONSUMER_KEY:$WC_CONSUMER_SECRET \
  -H "Accept: application/json"

get_product — single product with variations

curl -X GET "https://yourstore.com/wp-json/wc/v3/products/123" \
  -u $WC_CONSUMER_KEY:$WC_CONSUMER_SECRET

# For variable products, also fetch variations:
curl -X GET "https://yourstore.com/wp-json/wc/v3/products/123/variations" \
  -u $WC_CONSUMER_KEY:$WC_CONSUMER_SECRET

search_products — with filters

curl -X GET "https://yourstore.com/wp-json/wc/v3/products?search=wireless+headphones&category=42&min_price=20&max_price=100&stock_status=instock&per_page=10" \
  -u $WC_CONSUMER_KEY:$WC_CONSUMER_SECRET

check_inventory — variation-level

curl -X GET "https://yourstore.com/wp-json/wc/v3/products/123/variations/456" \
  -u $WC_CONSUMER_KEY:$WC_CONSUMER_SECRET
# Read: stock_status, stock_quantity, purchasable, backorders_allowed

initiate_checkout — POST /orders directly

This is the canonical headless agent flow: an authenticated server-side agent mints an order with line items, billing, shipping, payment method, and set_paid: true if the agent already has buyer authorization. Browser-driven agents that need a hosted page should hit the Store API /cart instead and redirect the buyer to /checkout/.

curl -X POST "https://yourstore.com/wp-json/wc/v3/orders" \
  -u $WC_CONSUMER_KEY:$WC_CONSUMER_SECRET \
  -H "Content-Type: application/json" \
  -d '{
    "payment_method": "stripe",
    "payment_method_title": "Credit Card",
    "set_paid": false,
    "billing": {
      "first_name": "Agent",
      "last_name":  "Buyer",
      "address_1":  "123 Main St",
      "city":       "Atlanta",
      "state":      "GA",
      "postcode":   "30301",
      "country":    "US",
      "email":      "buyer@example.com",
      "phone":      "404-555-0100"
    },
    "shipping": {
      "first_name": "Agent",
      "last_name":  "Buyer",
      "address_1":  "123 Main St",
      "city":       "Atlanta",
      "state":      "GA",
      "postcode":   "30301",
      "country":    "US"
    },
    "line_items": [
      { "product_id": 123, "variation_id": 456, "quantity": 1 }
    ],
    "shipping_lines": [
      { "method_id": "flat_rate", "method_title": "Flat Rate", "total": "5.99" }
    ],
    "meta_data": [
      { "key": "_agent_id",     "value": "claude-desktop-v1" },
      { "key": "_agent_origin", "value": "agent-test" }
    ]
  }'

get_order_status

curl -X GET "https://yourstore.com/wp-json/wc/v3/orders/789" \
  -u $WC_CONSUMER_KEY:$WC_CONSUMER_SECRET
# Read: status, date_paid, line_items, total, shipping_lines

request_return — refund creation

curl -X POST "https://yourstore.com/wp-json/wc/v3/orders/789/refunds" \
  -u $WC_CONSUMER_KEY:$WC_CONSUMER_SECRET \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "29.99",
    "reason": "Customer requested return via agent",
    "api_refund": true,
    "line_items": [
      { "id": 1234, "quantity": 1, "refund_total": 29.99 }
    ]
  }'

calculate_total — custom REST endpoint

WooCommerce does not ship a built-in "what would this cart cost, including tax and shipping, without creating an order" endpoint. The pattern is to register your own under /wp-json/agentmall/v1/calculate-total using the core REST API. Drop this in a site-specific plugin.

<?php
add_action( 'rest_api_init', function () {
    register_rest_route( 'agentmall/v1', '/calculate-total', [
        'methods'  => 'POST',
        'callback' => 'agentmall_calculate_total',
        'permission_callback' => function ( $request ) {
            // Reuse WC REST auth: presence of valid Basic Auth key
            return current_user_can( 'read' );
        },
    ] );
});

function agentmall_calculate_total( WP_REST_Request $request ) {
    $items = $request->get_param( 'line_items' );
    $country = $request->get_param( 'country' ) ?: 'US';
    $postcode = $request->get_param( 'postcode' ) ?: '';

    if ( ! is_array( $items ) ) {
        return new WP_Error( 'invalid_items', 'line_items required', [ 'status' => 400 ] );
    }

    $subtotal = 0;
    $detail   = [];

    foreach ( $items as $li ) {
        $product = wc_get_product( $li['variation_id'] ?? $li['product_id'] );
        if ( ! $product || ! $product->is_purchasable() ) { continue; }
        $qty   = (int) ( $li['quantity'] ?? 1 );
        $line  = (float) $product->get_price() * $qty;
        $subtotal += $line;
        $detail[] = [
            'product_id'   => $product->get_id(),
            'sku'          => $product->get_sku(),
            'quantity'     => $qty,
            'unit_price'   => (float) $product->get_price(),
            'line_total'   => $line,
        ];
    }

    // Tax estimate (simple flat rate fallback; production: use WC_Tax::calc_tax)
    $tax_rate = (float) get_option( 'agentmall_quote_tax_rate', 0.08 );
    $tax      = round( $subtotal * $tax_rate, 2 );

    // Shipping estimate
    $shipping = (float) get_option( 'agentmall_quote_shipping_flat', 5.99 );

    return [
        'currency'  => get_woocommerce_currency(),
        'line_items'=> $detail,
        'subtotal'  => round( $subtotal, 2 ),
        'tax'       => $tax,
        'shipping'  => $shipping,
        'total'     => round( $subtotal + $tax + $shipping, 2 ),
        'estimated' => true,
        'country'   => $country,
        'postcode'  => $postcode,
    ];
}

Edge rate limiting — nginx and Cloudflare

Pick one and ship it before agent traffic hits production. The nginx pattern is straightforward — drop a limit_req_zone definition in your http block and reference it inside your WooCommerce server block, scoped to /wp-json/wc/.

# /etc/nginx/conf.d/woocommerce-ratelimit.conf
limit_req_zone $binary_remote_addr zone=wc_api:10m rate=60r/m;

server {
    # ... existing config ...
    location /wp-json/wc/ {
        limit_req zone=wc_api burst=20 nodelay;
        try_files $uri $uri/ /index.php?$args;
    }
}

On Cloudflare, the equivalent lives under Security → WAF → Rate limiting rules: match on URI Path contains /wp-json/wc/, set a threshold (e.g. 60 requests per 1 minute per source IP), and choose Block or Managed Challenge. The free plan ships a basic version; the Pro plan unlocks per-second granularity and more rules. Re-verify Cloudflare plan limits before launch.

Tip · API Version Awareness

WooCommerce REST API versions (v1, v2, v3) are additive — v1 and v2 still exist but are flagged legacy for new integrations. Pin your agent code to v3 explicitly via the path prefix; never construct URLs that depend on an "unversioned" default. WooCommerce ships breaking changes infrequently on the REST surface, but auth and schema details have shifted between minor releases — read the release notes for any version bump that crosses a major.

For the full eight-endpoint pattern and the framework-agnostic OpenAPI spec, see the API Endpoint spoke.

The 30-Day AgentMall Newsletter

One operator note per week. The weekend build in your inbox.

Field-tested patterns, real failure modes, and the next platform spoke as it ships. No fluff. Cancel any time.

Layer 3 · MCP

10.3 native MCP in preview. Or a custom Python wrapper today.

WooCommerce 10.3 (shipped late 2025) introduced a native MCP integration as a developer-preview feature, disabled by default. The endpoint mounts at /wp-json/woocommerce/mcp and authenticates via an X-MCP-API-Key header carrying a WooCommerce REST consumer key:secret pair. The official @automattic/mcp-wordpress-remote npm package wires it into Claude Desktop without you writing transport code. For production today most operators ship a custom Python MCP wrapper over the WC v3 REST API and swap to native when the preview lifts. The two co-exist cleanly — register both in Claude Desktop and use whichever tool surface fits the call.

10.3 · Native Preview

/wp-json/woocommerce/mcp

Disabled by default in WooCommerce 10.3+. Enable via wp-cli or the woocommerce_features filter. Auth via X-MCP-API-Key: ck_...:cs_.... Developer preview — re-verify status before launch.

Wire-Up

@automattic/mcp-wordpress-remote

Official npm wrapper that bridges the native endpoint into Claude Desktop, Cursor, or any MCP client over stdio. Pass the API key as an env var.

Your Server · Custom

Custom Python MCP Wrapper

Production-stable today. Wraps the WC v3 REST API. Eight canonical tools. Add store-specific tools (loyalty, size charts, bundle pricing) the native server may not expose.

Enabling the 10.3 native MCP preview

The feature ships gated behind a feature flag. Two ways to flip it:

# Option 1: wp-cli (recommended for staging)
wp option update woocommerce_feature_mcp_integration_enabled yes

# Option 2: woocommerce_features filter in a site-specific plugin
add_filter( 'woocommerce_features', function( $features ) {
    $features['mcp_integration'] = true;
    return $features;
});

Confirm with a tools/list JSON-RPC POST to the endpoint, supplying your consumer key:secret as the API key header:

curl -X POST "https://yourstore.com/wp-json/woocommerce/mcp" \
  -H "Content-Type: application/json" \
  -H "X-MCP-API-Key: $WC_CONSUMER_KEY:$WC_CONSUMER_SECRET" \
  -d '{
    "jsonrpc": "2.0",
    "id": "1",
    "method": "tools/list"
  }'

The custom Python MCP wrapper — full source

Save as woocommerce_mcp_server.py, set WC_SITE_URL, WC_CONSUMER_KEY, and WC_CONSUMER_SECRET in your environment, run pip install mcp httpx, and register in Claude Desktop's config. This exposes the eight canonical tools over stdio.

#!/usr/bin/env python3
"""
woocommerce_mcp_server.py
A custom MCP server that wraps the WooCommerce v3 REST API and a small
custom /agentmall/v1/calculate-total endpoint. Production-stable today.
"""

import os
import asyncio
import httpx
from typing import Any, Optional

from mcp.server import Server, NotificationOptions
from mcp.server.models import InitializationOptions
from mcp.types import Tool, TextContent
import mcp.server.stdio

WC_SITE_URL = os.environ["WC_SITE_URL"].rstrip("/")          # e.g. https://yourstore.com
WC_KEY      = os.environ["WC_CONSUMER_KEY"]                  # ck_...
WC_SECRET   = os.environ["WC_CONSUMER_SECRET"]               # cs_...
WC_BASE     = f"{WC_SITE_URL}/wp-json/wc/v3"
AM_BASE     = f"{WC_SITE_URL}/wp-json/agentmall/v1"

server = Server("woocommerce-custom-mcp")

# --- Shared HTTP helpers -----------------------------------------------------

async def wc_get(path: str, params: Optional[dict] = None) -> Any:
    async with httpx.AsyncClient(timeout=15, auth=(WC_KEY, WC_SECRET)) as client:
        r = await client.get(f"{WC_BASE}{path}", params=params or {})
        r.raise_for_status()
        return r.json()

async def wc_post(path: str, payload: dict) -> Any:
    async with httpx.AsyncClient(timeout=20, auth=(WC_KEY, WC_SECRET)) as client:
        r = await client.post(f"{WC_BASE}{path}", json=payload)
        r.raise_for_status()
        return r.json()

async def custom_post(path: str, payload: dict) -> Any:
    async with httpx.AsyncClient(timeout=15, auth=(WC_KEY, WC_SECRET)) as client:
        r = await client.post(f"{AM_BASE}{path}", json=payload)
        r.raise_for_status()
        return r.json()

# --- Tool definitions --------------------------------------------------------

@server.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="list_products",
            description="List active, in-stock products. Paginated.",
            inputSchema={
                "type": "object",
                "properties": {
                    "per_page": {"type": "integer", "default": 20, "maximum": 100},
                    "page":     {"type": "integer", "default": 1},
                },
            },
        ),
        Tool(
            name="get_product",
            description="Get a single product by ID, including variations for variable products.",
            inputSchema={
                "type": "object",
                "properties": {"product_id": {"type": "integer"}},
                "required": ["product_id"],
            },
        ),
        Tool(
            name="search_products",
            description="Search products with optional category, price range, and stock filters.",
            inputSchema={
                "type": "object",
                "properties": {
                    "search":       {"type": "string"},
                    "category":     {"type": "integer"},
                    "min_price":    {"type": "number"},
                    "max_price":    {"type": "number"},
                    "stock_status": {"type": "string", "default": "instock"},
                    "per_page":     {"type": "integer", "default": 10, "maximum": 100},
                },
                "required": ["search"],
            },
        ),
        Tool(
            name="check_inventory",
            description="Variation-level inventory check. Returns stock_status, stock_quantity, purchasable.",
            inputSchema={
                "type": "object",
                "properties": {
                    "product_id":   {"type": "integer"},
                    "variation_id": {"type": "integer"},
                },
                "required": ["product_id"],
            },
        ),
        Tool(
            name="calculate_total",
            description="Quote total (subtotal + tax + shipping) for a candidate cart without creating an order.",
            inputSchema={
                "type": "object",
                "properties": {
                    "line_items": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "product_id":   {"type": "integer"},
                                "variation_id": {"type": "integer"},
                                "quantity":     {"type": "integer", "default": 1},
                            },
                            "required": ["product_id"],
                        },
                    },
                    "country":  {"type": "string", "default": "US"},
                    "postcode": {"type": "string"},
                },
                "required": ["line_items"],
            },
        ),
        Tool(
            name="initiate_checkout",
            description="Create a WooCommerce order. Server-side only — requires write-scoped key.",
            inputSchema={
                "type": "object",
                "properties": {
                    "line_items":       {"type": "array"},
                    "billing":          {"type": "object"},
                    "shipping":         {"type": "object"},
                    "payment_method":   {"type": "string", "default": "stripe"},
                    "payment_method_title": {"type": "string", "default": "Credit Card"},
                    "set_paid":         {"type": "boolean", "default": False},
                    "agent_id":         {"type": "string"},
                },
                "required": ["line_items", "billing"],
            },
        ),
        Tool(
            name="get_order_status",
            description="Fetch an order by ID. Returns status, totals, line items, payment + shipping detail.",
            inputSchema={
                "type": "object",
                "properties": {"order_id": {"type": "integer"}},
                "required": ["order_id"],
            },
        ),
        Tool(
            name="request_return",
            description="Create a refund against an order. Server-side only — requires write-scoped key.",
            inputSchema={
                "type": "object",
                "properties": {
                    "order_id":   {"type": "integer"},
                    "amount":     {"type": "string"},
                    "reason":     {"type": "string"},
                    "line_items": {"type": "array"},
                    "api_refund": {"type": "boolean", "default": True},
                },
                "required": ["order_id", "amount"],
            },
        ),
    ]

# --- Tool implementations ----------------------------------------------------

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    if name == "list_products":
        data = await wc_get("/products", {
            "per_page":     arguments.get("per_page", 20),
            "page":         arguments.get("page", 1),
            "status":       "publish",
            "stock_status": "instock",
        })
        lines = [f"{p['id']}  {p['sku']}  {p['name']}  {p['price']} {p.get('currency','USD')}" for p in data]
        return [TextContent(type="text", text="\n".join(lines) or "No products.")]

    if name == "get_product":
        pid = arguments["product_id"]
        prod = await wc_get(f"/products/{pid}")
        out = [f"#{prod['id']}  {prod['name']}  sku={prod['sku']}  price={prod['price']}  stock={prod['stock_status']}"]
        if prod.get("type") == "variable":
            vars_ = await wc_get(f"/products/{pid}/variations", {"per_page": 100})
            for v in vars_:
                out.append(f"  variation {v['id']}  sku={v['sku']}  price={v['price']}  stock={v['stock_status']}  qty={v.get('stock_quantity')}")
        return [TextContent(type="text", text="\n".join(out))]

    if name == "search_products":
        params = {
            "search":       arguments["search"],
            "per_page":     arguments.get("per_page", 10),
            "stock_status": arguments.get("stock_status", "instock"),
        }
        if "category" in arguments:  params["category"]  = arguments["category"]
        if "min_price" in arguments: params["min_price"] = arguments["min_price"]
        if "max_price" in arguments: params["max_price"] = arguments["max_price"]
        data = await wc_get("/products", params)
        lines = [f"{p['id']}  {p['name']}  {p['price']}  {p['stock_status']}" for p in data]
        return [TextContent(type="text", text="\n".join(lines) or "No results.")]

    if name == "check_inventory":
        pid = arguments["product_id"]
        vid = arguments.get("variation_id")
        if vid:
            data = await wc_get(f"/products/{pid}/variations/{vid}")
        else:
            data = await wc_get(f"/products/{pid}")
        ok = (data.get("stock_status") == "instock"
              and (data.get("stock_quantity") is None or data.get("stock_quantity", 0) > 0)
              and data.get("purchasable", True))
        return [TextContent(type="text", text=f"buyable={ok} stock_status={data.get('stock_status')} qty={data.get('stock_quantity')} purchasable={data.get('purchasable')}")]

    if name == "calculate_total":
        data = await custom_post("/calculate-total", {
            "line_items": arguments["line_items"],
            "country":    arguments.get("country", "US"),
            "postcode":   arguments.get("postcode", ""),
        })
        return [TextContent(type="text", text=f"subtotal={data['subtotal']} tax={data['tax']} shipping={data['shipping']} total={data['total']} {data['currency']}")]

    if name == "initiate_checkout":
        payload = {
            "payment_method":       arguments.get("payment_method", "stripe"),
            "payment_method_title": arguments.get("payment_method_title", "Credit Card"),
            "set_paid":             arguments.get("set_paid", False),
            "billing":              arguments["billing"],
            "shipping":             arguments.get("shipping", arguments["billing"]),
            "line_items":           arguments["line_items"],
            "meta_data": [
                {"key": "_agent_id",     "value": arguments.get("agent_id", "unknown")},
                {"key": "_agent_origin", "value": "mcp"},
            ],
        }
        order = await wc_post("/orders", payload)
        return [TextContent(type="text", text=f"order_id={order['id']} status={order['status']} total={order['total']} {order['currency']}")]

    if name == "get_order_status":
        oid = arguments["order_id"]
        order = await wc_get(f"/orders/{oid}")
        return [TextContent(type="text", text=f"order_id={order['id']} status={order['status']} total={order['total']} {order['currency']} date_paid={order.get('date_paid')}")]

    if name == "request_return":
        oid = arguments["order_id"]
        payload = {
            "amount":     arguments["amount"],
            "reason":     arguments.get("reason", ""),
            "api_refund": arguments.get("api_refund", True),
        }
        if "line_items" in arguments:
            payload["line_items"] = arguments["line_items"]
        refund = await wc_post(f"/orders/{oid}/refunds", payload)
        return [TextContent(type="text", text=f"refund_id={refund['id']} amount={refund['amount']} reason={refund.get('reason','')}")]

    return [TextContent(type="text", text=f"Unknown tool: {name}")]

# --- Entrypoint --------------------------------------------------------------

async def main():
    async with mcp.server.stdio.stdio_server() as (read, write):
        await server.run(
            read,
            write,
            InitializationOptions(
                server_name="woocommerce-custom-mcp",
                server_version="0.1.0",
                capabilities=server.get_capabilities(
                    notification_options=NotificationOptions(),
                    experimental_capabilities={},
                ),
            ),
        )

if __name__ == "__main__":
    asyncio.run(main())

Wire both servers into Claude Desktop

Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows) and register both the 10.3 native preview endpoint via the official npm bridge and your custom Python wrapper.

{
  "mcpServers": {
    "woocommerce-native": {
      "command": "npx",
      "args": ["-y", "@automattic/mcp-wordpress-remote", "--url", "https://yourstore.com/wp-json/woocommerce/mcp"],
      "env": {
        "X-MCP-API-Key": "ck_XXXXXXXXXXXXXXXXX:cs_YYYYYYYYYYYYYYYYY"
      }
    },
    "woocommerce-custom": {
      "command": "python3",
      "args": ["/absolute/path/to/woocommerce_mcp_server.py"],
      "env": {
        "WC_SITE_URL":        "https://yourstore.com",
        "WC_CONSUMER_KEY":    "ck_XXXXXXXXXXXXXXXXX",
        "WC_CONSUMER_SECRET": "cs_YYYYYYYYYYYYYYYYY"
      }
    }
  }
}
Why Two Servers, Not One

The 10.3 native server is in developer preview — schema and tool surface can shift between point releases, and the feature flag can be flipped server-side. Your custom Python wrapper is production-stable, ships the eight canonical tools you control, and can add store-specific logic the native server may never expose (loyalty tier lookups, bundle pricing, size charts). You get the platform velocity of the native endpoint and the stability of a custom server without re-implementing the canonical surface.

For the MCP protocol primer (JSON-RPC envelope, tool schemas, transport options) see the MCP spoke.

Layer 4 · UCP Compatibility

WooCommerce was not on the launch list. You assemble UCP yourself.

Be honest about where WooCommerce sits on Layer 4. The Universal Commerce Protocol (UCP) launched at NRF on January 11, 2026 as a Google + Shopify co-developed standard. The expanded partner set announced in April 2026 added more agent runtimes but did not add WooCommerce as a first-party integration. There is no /api/ucp/mcp endpoint that WooCommerce ships for you, no automatic catalog enrollment, and no plan tier that turns it on. What WooCommerce did ship: 10.7 (April 14, 2026) added a native agentic_commerce payment gateway flag that implements ACP-style flows — the closest first-party hook to the UCP standard. For UCP proper, you publish your own /.well-known/ucp JSON manifest, mount the UCP catalog + checkout state machine on top of the WC v3 REST API, and register with UCP-conformant agents directly. It is the most work of the four layers — and the most honest place on the page.

Where WooCommerce stands on the agent-protocol landscape

ProtocolWooCommerce StatusPath
UCP (Universal Commerce Protocol)Not on Jan 2026 launch; not on April 2026 expansionHand-rolled /.well-known/ucp manifest + REST mapping
ACP (Agentic Commerce Protocol)Native agentic_commerce payment gateway flag in 10.7 (Apr 14, 2026)Enable flag in your gateway; wire woocommerce_checkout_create_order hook
MCP (Model Context Protocol)Native preview at /wp-json/woocommerce/mcp (10.3+, disabled by default)Enable feature flag; auth via X-MCP-API-Key
Schema.org ProductAuto-emitted by core + Yoast/RankMath; you replace with custom snippetwp_head PHP snippet (Layer 1)

The eight-step UCP state machine — mapped to WooCommerce

Even without a first-party UCP endpoint, your custom MCP server and your REST mapping should track the eight UCP states so your error messages, your _agent_id meta, and your webhook payloads line up cleanly when WooCommerce or a third-party plugin does ship UCP support.

#UCP StateWooCommerce BackingAgent Action
1discoverGET /wp-json/wc/v3/productsFind candidate products
2inspectGET /wp-json/wc/v3/products/{id} + /variationsRead details, variants, attributes
3check_availabilitystock_status + stock_quantity + purchasableConfirm buyable variation
4build_intentClient-side cart object — no server state until POST /ordersAssemble candidate cart
5quotePOST /wp-json/agentmall/v1/calculate-totalRead estimated total incl. tax + shipping
6confirmBuyer authorization (ACP gateway in 10.7+, or browser handoff)Lock buyer identity + payment intent
7executePOST /wp-json/wc/v3/orders with set_paid or redirect to /checkout/Complete order
8trackGET /wp-json/wc/v3/orders/{id} + order.updated webhookSurface fulfillment status

The /.well-known/ucp manifest

Publish a JSON manifest at https://yourstore.com/.well-known/ucp so UCP-conformant agents can discover your endpoints. Serve as static JSON from your web root, or register a tiny REST route under agentmall/v1/well-known-ucp and rewrite the path with an nginx or .htaccess rule.

{
  "ucp_version": "2026-04-08",
  "store": {
    "name": "Your Store",
    "url":  "https://yourstore.com",
    "currency": "USD",
    "country":  "US"
  },
  "endpoints": {
    "catalog":   "https://yourstore.com/wp-json/wc/v3/products",
    "product":   "https://yourstore.com/wp-json/wc/v3/products/{id}",
    "quote":     "https://yourstore.com/wp-json/agentmall/v1/calculate-total",
    "checkout":  "https://yourstore.com/wp-json/wc/v3/orders",
    "order":     "https://yourstore.com/wp-json/wc/v3/orders/{id}",
    "refund":    "https://yourstore.com/wp-json/wc/v3/orders/{id}/refunds",
    "mcp":       "https://yourstore.com/wp-json/woocommerce/mcp"
  },
  "auth": {
    "scheme": "basic",
    "scopes": ["read_products", "read_orders", "write_orders", "write_refunds"]
  },
  "agentic_commerce": {
    "supported": true,
    "since":     "WooCommerce 10.7",
    "gateway":   "agentic_commerce"
  }
}

The 10.7 ACP hooks

WooCommerce 10.7 added a native agentic_commerce payment gateway flag. The two hooks you need to write — even if your gateway already declares the flag — are woocommerce_checkout_create_order (to stamp _agent_id on every agent-originated order) and woocommerce_payment_complete (to fire your agent telemetry pipeline when payment lands). Drop these in your site-specific plugin.

<?php
// Stamp agent identity on every order an agent initiates
add_action( 'woocommerce_checkout_create_order', function ( $order, $data ) {
    $agent_id = $_SERVER['HTTP_X_AGENT_ID'] ?? '';
    if ( $agent_id ) {
        $order->update_meta_data( '_agent_id', sanitize_text_field( $agent_id ) );
        $order->update_meta_data( '_agent_origin', 'acp' );
    }
}, 10, 2 );

// Fire telemetry when an agent-originated payment lands
add_action( 'woocommerce_payment_complete', function ( $order_id ) {
    $order    = wc_get_order( $order_id );
    $agent_id = $order ? $order->get_meta( '_agent_id' ) : '';
    if ( $agent_id ) {
        do_action( 'agentmall_agent_payment_complete', $order_id, $agent_id );
    }
});
Critical · UCP Listing Is Not Automatic

WooCommerce does not enroll your store in the UCP catalog the way Shopify Catalog does. Publishing the .well-known/ucp manifest above makes you discoverable to UCP-conformant agents that crawl it, but does not automatically place you in ChatGPT shopping, Microsoft Copilot, or Google AI Mode the way a Shopify Catalog enrollment does. Track each agent runtime's merchant onboarding path separately and re-verify before launch — and budget for a partner-program application cycle for the major platforms.

For the full UCP state machine, agent profile schema, and the cross-platform protocol detail see the UCP spoke.

§7 · Honest Trade-offs

What WooCommerce makes easy. What it makes hard.

Easy

CapabilityWhy It Is Easy
Real REST API on every install/wp-json/wc/v3/ ships with WooCommerce; no add-on, no plan tier
Native global_unique_idGTIN/UPC/EAN is a first-class field since WooCommerce 8.x
Direct order creationPOST /orders with set_paid creates and pays in one call
Open codeYou own functions.php — any schema, hook, or endpoint is a snippet away
Custom REST endpointsregister_rest_route in 10 lines for a calculate_total or any custom tool
WebHooks built inorder.created, order.updated, product.updated configurable in admin UI
WP Application PasswordsCore since 5.6 — short-lived per-app credentials for custom endpoints

Hard

CapabilityWhy It Is Hard
Rate limiting on v3 RESTZero native rate limiting — must push to Cloudflare or nginx at the edge
UCP listingWooCommerce absent from Jan 2026 launch and April 2026 expansion; hand-rolled manifest
Duplicate Product schemaYoast + RankMath + theme all emit it; you disable two of three
Hosting variancePerformance is your host's job — agent traffic exposes bad PHP-FPM config and DB tuning
Plugin sprawl50-plugin stores break in ways a managed platform never does; audit before agent launch
Variable product UX for agentsAgents must resolve to a specific variation_id; never quote the parent SKU as buyable
Custom checkout flowsSubscription, deposits, B2B custom flows do not always honor agent set_paid
§8 · WordPress Hosting

Managed WooCommerce hosts that survive agent traffic.

Agent traffic is not like organic traffic — it spikes inside a single buyer session (search → inspect → variation lookup → quote → order in under 60 seconds), and a misconfigured agent loops on errors instead of giving up like a human does. The shared-hosting tier most WooCommerce stores start on (low PHP worker counts, default OPcache, no edge cache for /wp-json/*) will time out under a real agent test. The managed-WooCommerce tier is the floor. Re-verify every price below before launch — host pricing tiers and included features change often.

HostEntry PlanNotes
Kinsta~$35/mo Starter; ~$115/mo Business 1 (re-verify before launch)Google Cloud Platform infrastructure; per-site PHP worker tuning; built-in object cache; APO-compatible CDN edge
WP Engine~$30/mo Startup (re-verify before launch)Mature WooCommerce-specific tier on higher plans; EverCache page cache; staging environments included
Cloudways~$14-46/mo (DigitalOcean 4GB ~$46/mo — re-verify before launch)Pick your IaaS (DO, Vultr, Linode, AWS, GCP); managed PHP-FPM + Redis; cheaper headroom for variable agent traffic
Hostinger Business WooCommerce~$3.99 intro / ~$18.99 renewal (re-verify before launch)Lowest-cost managed tier with WooCommerce optimizations; fine for low-volume launch, plan an upgrade path
Pressable / Pagely / WordPress.com VIPQuote-based (re-verify before launch)Enterprise tier; SLA-backed performance and security; reserved for high-volume merchants with engineering teams
Tip · Exclude /wp-json/ from Page Caching

Every full-page cache (host-level, plugin, or CDN edge) must be configured to bypass /wp-json/*. A cached REST response that returns stale stock_quantity or price values to an agent will create carts for items that are no longer available — and the agent has no way to know. Confirm in your host dashboard and in any Cloudflare page-rule or Cache-Rule configuration before launch.

§9 · End-to-End Verification

Test it with Claude Desktop. Real order, real refund.

You have a wp_head Schema snippet emitting JSON-LD, a WC v3 REST API with consumer keys, edge rate limiting, the 10.3 native MCP preview enabled (or the Python wrapper deployed), and the ACP hooks wired. The last weekend hour is the end-to-end test: a real agent, your real store, a real order in WooCommerce admin. Use Claude Desktop with both MCP servers registered. The eight-tool walkthrough below exercises every layer you built.

Setup

  1. Confirm claude_desktop_config.json contains both woocommerce-native and woocommerce-custom entries (see Layer 3).
  2. Restart Claude Desktop fully — quit from the menu bar, do not just close the window.
  3. Verify the tool icons appear in a new chat. You should see tools from both servers.
  4. Have a real product in your store with at least one in-stock variation and a non-zero quantity.
  5. Have a Stripe (or your gateway's) test mode enabled so test orders do not charge real money.

The eight-tool prompt walkthrough

#Prompt to ClaudeWhat Should Happen
1"List products in my WooCommerce store, in stock, under $50."Claude calls list_products (custom) or search_products with stock + price filters; returns IDs, SKUs, prices.
2"Show me variation details for the first result."Claude calls get_product and lists every variation with SKU, price, stock_status, stock_quantity.
3"Search my store for wireless headphones between $30 and $80."Claude calls search_products with min_price/max_price and returns filtered list.
4"Confirm the first variation is actually buyable using check_inventory."Claude calls check_inventory and returns buyable=true with stock_status and quantity.
5"Calculate the total if I buy one of those variations, shipping to 30301."Claude calls calculate_total and returns subtotal + tax + shipping + total.
6"Create a test order for that variation with set_paid=false and tag it agent-test."Claude calls initiate_checkout and returns an order_id with status pending or processing.
7"Look up the status of order_id {N}."Claude calls get_order_status and returns status, total, and date_paid.
8"Refund $5.00 of that order with reason 'agent test refund'."Claude calls request_return against /orders/{id}/refunds and returns refund_id + amount.
Tip · Filter Reports by _agent_id

Because the Layer 4 hook stamps _agent_id on every agent-originated order, you can isolate agent-driven revenue in WooCommerce Admin → Orders by filtering on the meta field. Pair with a SELECT on wp_postmeta for sharper breakdowns: revenue by agent runtime (Claude vs ChatGPT vs Copilot), refund rate, average order value, time-to-fulfill. This becomes your baseline metric for the first 90 days post-launch.

What to log

§10 · Plugin Roundup

WooCommerce plugins that shortcut the build.

Every price below is flagged "re-verify before launch" — WooCommerce plugin tiers and renewal pricing change, and a free tier may be capped to feature scope, site count, or update window. Confirm on each listing before purchasing.

PluginWhat It DoesLayerPrice (re-verify before launch)
Yoast WooCommerce SEOProduct schema + sitemaps + meta tags + breadcrumbs; safe enterprise defaultLayer 1~$178.80/yr bundled
RankMath ProMost complete WooCommerce schema; unlimited sites; AI title/meta generationLayer 1~$84/yr Pro
Schema ProFAQ, HowTo, Article, Review schema in addition to ProductLayer 1~$79/yr intro / ~$249 lifetime
All In One SEO ProSchema + meta + sitemap suite; WooCommerce module bundledLayer 1~$199.50/yr
WPSSO WC Metadata ProPer-variation Offer schema and granular shipping/policy fieldsLayer 1~$59.99/yr
Matrixify (Excelify)Bulk CSV/XLSX import-export including global_unique_id, _brand, _mpn metaLayer 1Tiered — free starter + paid plans (re-verify)
JWT Authentication for WP-APIShort-lived JWT tokens you can hand to agents and revoke independentlyLayer 2Free
WP OAuth ServerOAuth 2.0 provider for headless and multi-tenant agent scopesLayer 2Free core / ~$149 Pro
Wordfence SecurityWAF, login hardening, rate limit floor, malware scanLayer 2Free / ~$119/yr Premium
Action Scheduler (bundled with WooCommerce)Background queue for webhooks and async telemetry — already installedLayer 4Free

The picks-and-shovels equivalents that work across platforms (not WooCommerce-specific) are catalogued on the Picks & Shovels spoke.

§11 · Common Mistakes

Eight ways WooCommerce agent integrations break in production.

1. Running API keys over HTTP instead of HTTPS

Basic Auth with ck_/cs_ sends the consumer key and secret in every request as a base64-encoded header. Over plain HTTP, any network hop can read them — and once leaked, an attacker can drain inventory, mint orders, or change product prices via the REST API. Always enforce HTTPS at the server (HSTS, redirect HTTP-to-HTTPS, terminate TLS at the edge). On Cloudflare set SSL/TLS mode to Full (strict). Rotate any key that has ever transited HTTP.

2. Issuing read/write keys to read-only agents

A buyer-facing agent that only reads catalog and inventory should never hold a write-scoped key. Scope every key to the minimum capability set: Read for browsing agents, Read/Write only behind your own authenticated backend proxy. Generate a separate key per agent runtime (Claude vs ChatGPT vs Copilot) so a single compromise has a small blast radius and you can revoke without disturbing the others.

3. Duplicate Schema.org Product blocks (Yoast + custom + theme)

Stacking Yoast WooCommerce SEO's Product schema, RankMath Pro's Product schema, the theme's built-in JSON-LD, and your custom wp_head snippet produces two or three conflicting Product blocks per page. Google's Rich Results Test flags this as a structured-data error, and most agent shopping platforms quietly drop your products from results. Pick exactly one source, disable the others under each plugin's schema settings, and re-validate.

4. Leaving v3 REST API rate limiting unaddressed

The biggest production gap on WooCommerce. The v3 REST API has no native rate limiting; the Store API has an optional 25 req/10s default that is disabled. A misbehaving agent — or one with a valid key being abused — will saturate PHP-FPM and the database without ever tripping a 429. Push rate limiting to Cloudflare WAF rules, an nginx limit_req_zone, or your managed-host WAF before launch, keyed on the consumer key prefix so a single bad agent does not break the rest.

5. Caching /wp-json/* REST responses at CDN edge

If your CDN page-cache rules or Cloudflare Cache Rules include /wp-json/*, an agent that reads stock_quantity: 1 will keep getting 1 back even after the unit sells — and will create carts for items that no longer exist. Configure every page cache (host, plugin, CDN) to bypass /wp-json/*. The right place to cache REST traffic is inside PHP via transient or object-cache helpers, not at the edge.

6. Not populating global_unique_id (GTIN)

WooCommerce 8.x+ ships global_unique_id as a native field, but most migrated stores leave it empty. Without GTIN/UPC/EAN coverage, Google AI Overviews demotes your products and most AI shopping agents exclude them from results entirely. Bulk-import from your supplier feed via Matrixify or a WP-CLI script before launch.

7. Using shortcode checkout [woocommerce_checkout] for headless flows

The classic [woocommerce_checkout] shortcode and the modern Block Checkout are human paths, not agent paths. A headless agent that tries to scrape, drive, or POST to the checkout page will hit nonces, CSRF tokens, and session-cookie state that does not survive an out-of-browser context. For fully headless agent flows mint orders directly via POST /wp-json/wc/v3/orders with set_paid. For browser-redirect agent flows use the Store API /cart endpoints and redirect to /checkout/ as a fallback.

8. Not handling variable products in Schema markup

A variable product like a t-shirt with size and color variants is one parent SKU and N buyable variations. Your Schema.org Product block must emit one Offer entry per in-stock, purchasable variation — not a single Offer for the parent. The wp_head PHP snippet in Layer 1 loops through get_children() for exactly this reason. Agents that conflate parent and variations end up presenting unbuyable parent SKUs to buyers and creating carts that fail at checkout.

§12 · FAQ

Frequently asked questions.

Does WooCommerce have a native MCP server today?

Yes — WooCommerce 10.3 (shipped late 2025) introduced a native MCP integration as a developer-preview feature, disabled by default. You enable it by flipping the woocommerce_feature_mcp_integration_enabled option or by filtering woocommerce_features, after which an MCP endpoint becomes available at /wp-json/woocommerce/mcp. Authentication uses an X-MCP-API-Key header carrying a WooCommerce REST consumer key:secret pair, and the official @automattic/mcp-wordpress-remote npm package wires it into Claude Desktop. Because it is a developer preview, do not treat it as production-stable; many operators ship a custom Python MCP wrapper over the WC v3 REST API today and switch to native when it leaves preview. Re-verify the preview status and exact option name against the current WooCommerce release notes before launch.

How do I authenticate WooCommerce REST API requests from an agent?

The WooCommerce-native path is HTTP Basic Auth over HTTPS using a consumer key (ck_...) and consumer secret (cs_...) generated under WooCommerce → Settings → Advanced → REST API. OAuth 1.0a one-legged is supported for non-HTTPS legacy environments but is no longer recommended. WordPress Application Passwords (built into core since 5.6) work for the broader WP REST API and any custom endpoints you expose under /wp-json/agentmall/v1/, and the JWT Authentication for WP-API plugin is the right call when you need short-lived tokens you can hand to an agent and revoke independently. For agent traffic specifically, scope every key tightly: a buyer-facing agent should only ever hold a read-only key, and write-scoped keys should live behind your own authenticated backend proxy.

Can an agent complete a purchase end-to-end on WooCommerce?

Programmatically, yes — POST /wp-json/wc/v3/orders with line_items, billing, shipping, payment_method, and set_paid creates and pays an order in a single call, which is how an agent operating with a server-side write key can complete a purchase without the buyer ever loading a checkout page. For trusted-agent flows you can additionally wire the WooCommerce 10.7 agentic_commerce payment gateway flag plus an _agent_id order meta field via woocommerce_checkout_create_order. The shortcode checkout at [woocommerce_checkout] is not the agent path — that is the human path. For browser-native agents that need a hosted page, generate a Store API cart and redirect the buyer to /checkout/; for fully headless agent flows, use the Orders endpoint directly. Re-verify the 10.7 ACP gateway flag and your processor's agent-on-behalf rules before going live.

What is UCP and does WooCommerce support it natively?

The Universal Commerce Protocol (UCP) is the Google + Shopify co-developed agent-commerce standard that launched at NRF on January 11, 2026, with an expanded partner set announced in April 2026. WooCommerce was not on either launch list and does not ship native UCP today. You can still expose a UCP-conformant surface yourself by publishing a /.well-known/ucp JSON manifest, mounting the UCP catalog and checkout state machine on top of the WC v3 REST API, and registering with UCP-conformant agents directly. WooCommerce 10.7 (April 14, 2026) did add a native agentic_commerce payment gateway flag for ACP-style flows, which is the closest first-party hook — pair it with a hand-rolled UCP manifest for the discovery side. Re-verify both the WooCommerce 10.7 release notes and the current UCP spec before launch.

Why is there no rate limiting on the WC v3 REST API?

WooCommerce core ships no native rate limiting on /wp-json/wc/v3/ — that endpoint will accept whatever traffic your PHP-FPM workers and database can serve, which is the single biggest operational gap an agent integration has to close before launch. The newer Store API at /wp-json/wc/store/v1/ ships an optional 25-requests-per-10-seconds default that is disabled out of the box and can be tuned via the woocommerce_store_api_rate_limit_options filter, but it does not cover the v3 surface agents actually call. The production fix is to put rate limiting at the edge — Cloudflare WAF rate-limiting rules, an nginx limit_req_zone in front of PHP-FPM, or a managed-host WAF layer — keyed on the consumer key prefix so a misbehaving agent is throttled without affecting the rest of the API. Re-verify Cloudflare plan limits and your host's WAF policy before launch.

How do I handle variable product inventory for agents?

A variable product in WooCommerce is the parent SKU, and the buyable units are the variations beneath it — each with its own SKU, price, stock, and attribute combination. For agents, never expose the parent stock_status as buyable; always resolve to a specific variation_id via GET /wp-json/wc/v3/products/{id}/variations and check that variation's stock_status, stock_quantity, and purchasable fields before offering it for a cart. Your Schema.org Product block must emit one Offer entry per in-stock, purchasable variation (the wp_head PHP snippet in Layer 1 loops through get_children() for exactly this reason). Backorder-allowed variations should be surfaced separately as BackOrder availability, not InStock — agents that conflate the two end up creating carts for items the merchant cannot actually ship on the buyer's quoted timeline.

Which SEO or Schema plugin should I use for WooCommerce?

Pick exactly one Product schema source — the most common production mistake is layering Yoast WooCommerce SEO, RankMath Pro, and a theme's built-in JSON-LD, which emits two or three conflicting Product blocks per page and breaks Google Rich Results validation. If you own the functions.php and want full control of the 20-field MVP, use the custom wp_head PHP snippet in Layer 1 and turn off the schema output in whatever SEO plugin you keep for sitemaps and meta tags. If you want a managed path, RankMath Pro (around $84/year for unlimited sites — re-verify before launch) ships the most complete WooCommerce schema out of the box; Yoast WooCommerce SEO (around $178.80/year bundled — re-verify before launch) is the safe enterprise default; Schema Pro is the right call when you need schema for non-product page types (FAQ, Article, HowTo) alongside the WooCommerce surface. Whichever you pick, validate every product template at Google's Rich Results Test before deploy.

Does the WC v3 REST API work with headless WordPress setups?

Yes — the REST API ships independently of the WordPress theme layer, so a headless front-end built on Next.js, Astro, Remix, or any other framework can drive the full /wp-json/wc/v3/ surface as long as pretty permalinks are enabled and the consumer key has the right scopes. Headless setups make the rate-limiting gap more acute because every front-end render path can hit the API directly, so the Cloudflare or nginx edge-throttling step in Layer 2 is non-negotiable. The Store API at /wp-json/wc/store/v1/ uses cookie-based cart state that does not work cleanly across a decoupled front-end; for headless agent flows, mint orders directly via POST /wp-json/wc/v3/orders, return the order key, and skip the Store API cart entirely. WPGraphQL plus the WPGraphQL for WooCommerce extension is a viable alternative GraphQL surface but adds a separate auth and rate-limit story you also have to harden.

§13 · Step-by-Step

The weekend retrofit, in five steps.

Each step mirrors the HowTo JSON-LD at the top of this page word for word. Execute in order. The whole sequence should fit in two evenings or one full Saturday for a store that already has clean product data; budget a second weekend for stores migrating from a non-WooCommerce platform.

Step 1 — Audit product data and deploy the wp_head PHP JSON-LD snippet

Open functions.php in a child theme or a site-specific plugin and add the agentmall_product_jsonld() snippet from Layer 1, which hooks wp_head at priority 99 and emits the 20-field Schema.org Product block. Populate the global_unique_id field (GTIN/UPC/EAN) on every product via the native WooCommerce 8.x+ product editor field or a Matrixify CSV import, then disable Yoast or RankMath Product schema output to avoid duplicate blocks. Validate every product template at Google's Rich Results Test and the Schema.org Validator before deploying.

Step 2 — Generate consumer key/secret and add rate limiting at Cloudflare or nginx

In WooCommerce → Settings → Advanced → REST API, click Add Key, scope it Read for buyer-facing agents or Read/Write for server-side automation, and save the consumer key and secret immediately (the secret is only shown once). Because the v3 REST API has no native rate limiting, add a Cloudflare WAF rate-limiting rule (free plan ships a basic version) or an nginx limit_req_zone keyed on the consumer key prefix at 60 requests per minute for read keys. Run the list_products curl from Layer 2 against /wp-json/wc/v3/products and confirm a real JSON response with the 20-field MVP surface.

Step 3 — Enable WooCommerce 10.3+ native MCP integration or deploy the Python MCP wrapper

On WooCommerce 10.3 or later, enable the developer-preview native MCP feature with wp option update woocommerce_feature_mcp_integration_enabled yes (or via the woocommerce_features filter), then confirm the endpoint at /wp-json/woocommerce/mcp responds to a tools/list JSON-RPC POST. If you need custom tools — size charts, loyalty lookups, bundle pricing — or want a production-stable surface today, deploy the woocommerce_mcp_server.py from Layer 3 alongside the native endpoint. Both servers register in Claude Desktop and can be called in the same agent session.

Step 4 — Configure Claude Desktop and run the 8-tool staging verification

Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows) and add both the native /wp-json/woocommerce/mcp endpoint (via @automattic/mcp-wordpress-remote with X-MCP-API-Key) and the Python wrapper. Restart Claude Desktop fully and walk through the eight-tool verification table in §9: list_products, get_product, search_products, check_inventory, calculate_total, initiate_checkout, get_order_status, and request_return. Tag the resulting test orders with agent-test so you can isolate agent-originated revenue in WooCommerce reports.

Step 5 — Wire ACP/UCP hooks plus webhooks and lock down with Wordfence + key scoping

Add the woocommerce_checkout_create_order and woocommerce_payment_complete hooks from Layer 4 so every agent-originated order gets an _agent_id meta field; enable the WooCommerce 10.7 agentic_commerce payment gateway flag for ACP flows; publish a /.well-known/ucp JSON manifest so UCP-conformant agents can discover your endpoints. Register webhooks for order.created, order.updated, and product.updated under WooCommerce → Settings → Advanced → Webhooks. Install Wordfence (free, or ~$119/year Premium — re-verify before launch), restrict /wp-json/wc/v3/ to your agent key prefixes, and confirm Cloudflare rate limiting is live before flipping the agent traffic switch.

§14 · Continue the Guide

Next stops in the AgentMall guide.

Sibling · Platform

The Shopify Sibling Spoke

The weekend build for Shopify — Liquid schema, Storefront API, the native /api/mcp, and Shopify Catalog's automatic UCP enrollment. Read it for the contrast: WooCommerce is the retrofit, Shopify is the unlock.

Layer 4 · Protocol

UCP — Universal Commerce Protocol

The eight-step state machine, agent profile schema, and the cross-platform contract that powers ChatGPT, Copilot, and Google AI Mode — including the .well-known/ucp manifest you self-host on WooCommerce.

Layer 3 · Protocol

MCP — Model Context Protocol

Tool schemas, JSON-RPC envelope, transports, and the framework you implement once and reuse across Claude, ChatGPT, Cursor, and any future agent runtime.

Layer 1 · Reference

Product Data — The 20-Field MVP

Field-by-field reference for the Schema.org block that gets you discoverable across every agent runtime, with mapping tables for Shopify, WooCommerce, and BigCommerce.

Layer 2 · Build

API Endpoint — The Eight-Endpoint Surface

The framework-agnostic, OpenAPI-driven endpoint pattern. FastAPI + Vercel reference implementation. Drop-in for any non-Woo stack and the contract your Woo custom REST endpoints must satisfy.

Market

The Agent Commerce Market

TAM, channel mix, and the buyer-side data that lets you size the agent-commerce opportunity before you ship a single tool.

Stack

Free vs Paid Stack Choices

Where the free tiers run out and which paid services (managed WP host, RankMath Pro, Wordfence Premium, Cloudflare Pro) are worth the upgrade for a real production agent integration.

Stack

The Agent-Commerce Stack

End-to-end reference stack — storefront, schema, API, MCP, UCP, observability — for operators building greenfield rather than retrofitting an existing WooCommerce store.

Tools

Picks & Shovels

The cross-platform tools (review collectors, schema validators, MCP scaffolds, monitoring) that work the same whether you are on WooCommerce, Shopify, BigCommerce, or custom.

The Window

The window for agent-ready WooCommerce stores is now.

Google + Shopify co-launched UCP at NRF on January 11, 2026 and expanded partners in April 2026 — WooCommerce was not on either launch list, and the 10.7 release that shipped April 14, 2026 added ACP hooks but not UCP discovery. That gap is the opportunity: every WooCommerce store that closes Schema, REST API rate limiting, MCP, and a self-hosted UCP manifest this weekend ships a surface that the platform itself does not provide out of the box, and the platform's quarterly release cadence means the floor keeps moving. Operators who retrofit now get the compounding catalog placement, the trusted-store status, and the operator-class agent telemetry. Operators who wait will spend the back half of 2026 catching up while their Shopify-running peers extend their lead.

Open the AgentMall Roadmap →

One AgentMall note per week.

Platform-specific weekend builds, real failure modes from operator logs, and the next spoke the morning it ships. No fluff. Cancel any time.