wc-rest-api-v4

Use the WooCommerce REST API v4 (namespace wc/v4, since WC 10.2) alongside or in place of v3 — verified route catalog (Customers, Orders, Refunds, Products, ShippingZones with DELETE, ShippingZoneMeth

SKILL.md

GitHub

WooCommerce REST API v4

A second-major-version of the WC REST API, introduced in WC 10.2 (2025) and expanded through 10.8. It coexists with v3 — neither replaces the other — and adds capabilities the v3 surface lacked: per-route response caching, ID-sortable customer list, DELETE on shipping zones / methods, payment gateway PUT with top-level fields, fulfillments CRUD, per-group settings endpoints, and stricter 10.8 order/product response behavior.

This skill is the up-to-date reference for AI assistants whose training data predates v4. Default behavior of LLMs is to write /wc/v3/... URLs and v3 controller patterns. Many of those endpoints exist in v4 with cleaner shapes, additional verbs, and per-endpoint cache headers — and a few v4 endpoints have NO v3 counterpart.

Misconception this skill corrects

Two common AI errors:

  1. "I'll write /wp-json/wc/v3/... because that's the WC REST API." v3 still works, but v4 may be the right answer for ID-list parameters on customers, DELETE on shipping zones, modern fulfillments, payment gateway settings. Check the route catalog below.

  2. "I'll extend Automattic\WooCommerce\Internal\RestApi\Routes\V4\AbstractController for my plugin's routes." The class lives under Internal\ — that's WC core's signal "no public extension contract here, the internal shape can change in any minor release." Plugin-defined routes use WP_REST_Controller directly (see sibling skill wp-rest-api). The v4 AbstractController patterns are educational; the class itself is not a stable extension point.

When to use this skill

Trigger when ANY of the following is true:

  • Calling WC REST endpoints from a custom plugin, external integration, or custom client.
  • Reviewing hardcoded /wc/v3/ URLs to check whether v4 has a better-shaped endpoint for the same data.
  • Building a new feature that needs ID-sortable customers, fulfillments, granular settings, or per-route cache headers.
  • Debugging "why is my v3 customers query slow" — v4 added caching infrastructure for these.
  • The diff or file contains: /wc/v4/, wc/v4, woocommerce_rest_api_v4_*, AbstractController in WC context, RestApiCache, with_cache(.

Route catalog (verified in WC 10.8 source)

All routes live under namespace wc/v4. Verified by directory listing of wp-content/plugins/woocommerce/src/Internal/RestApi/Routes/V4/.

v4 routes are FLAT, NOT nested under parent resources. Order notes, refunds, and fulfillments take an order_id query parameter — they don't appear under /orders/<id>/.... Verified in each Controller's register_rest_route call:

Resource Path Notes
Customers /wc/v4/customers, /wc/v4/customers/<id> ID-sortable; customers_exclude query param. Username and password optional regardless of registration settings.
Orders /wc/v4/orders, /wc/v4/orders/<id> HPOS-aware; cache-primed for batch reads.
Order Notes /wc/v4/order-notes?order_id=<id>, /wc/v4/order-notes/<note_id> Flat path; pass order_id as query param to filter by order. Stored XSS prevention for note content (10.7 patch).
Refunds /wc/v4/refunds?order_id=<id>, /wc/v4/refunds/<refund_id> Same flat-path pattern. Floating-point precision fix (10.7).
Products /wc/v4/products, /wc/v4/products/<id>
Shipping Zones /wc/v4/shipping-zones, /wc/v4/shipping-zones/<id> Note: dash-separated rest_base, NOT /shipping/zones/. DELETE supported in v4. Locations array optional.
Shipping Zone Method (single) /wc/v4/shipping-zone-method/<instance_id> Flat path with the instance ID — NOT nested under a zone. New endpoint type in v4.
Fulfillments /wc/v4/fulfillments?order_id=<id>, /wc/v4/fulfillments/<fulfillment_id> Flat path. New system in WC 10.7. Lifecycle order notes auto-generated.
Fulfillment Providers /wc/v4/fulfillments/providers Sub-route under fulfillments. List of registered AbstractShippingProvider subclasses.
Settings (general) /wc/v4/settings/general
Settings (account) /wc/v4/settings/account
Settings (tax) /wc/v4/settings/tax
Settings (email/emails) /wc/v4/settings/email, /wc/v4/settings/emails Block email editor backend.
Settings (payment gateways) /wc/v4/settings/payment-gateways PUT accepts top-level enabled, title, description, order (10.7) — symmetric with GET.
Settings (offline payment methods) /wc/v4/settings/offline-payment-methods BACS / cheque / COD config.
Settings (products) /wc/v4/settings/products

The Internal\RestApi\Routes\V4\ namespace maps each resource to its Controller. Verified rest_base values: customers, orders, order-notes, refunds, products, shipping-zones, shipping-zone-method, fulfillments. The actual URL is the namespace + the rest_base; nothing more (until you hit a sub-route like /fulfillments/providers).

WC 10.8 behavior changes

  • status=any order listings exclude checkout-draft. If an integration needs draft checkout orders, request them explicitly with status=checkout-draft. WC 10.8 aligns this with statuses marked exclude_from_search, and HPOS query code comments call out checkout-draft specifically.
  • V4 product responses strip sensitive fields for users without product-management capabilities. Downloads, COGS, purchase notes, and raw meta_data are removed when the current user can read products but cannot manage/private-read products. Do not build external clients that depend on those fields unless the request is authenticated with the right capability.
  • Order/refund tax fields were clarified. Inline refund data includes total_tax, and schema descriptions distinguish tax-inclusive/exclusive totals. Client-side financial calculations should trust explicit fields instead of inferring tax from display strings.
  • Guest fulfillments access was tightened. Treat fulfillment endpoints as capability-protected admin/integration APIs; do not mirror them into customer-facing screens without ownership checks.
  • Legacy v2/v3 order PUT is stricter. /wc/v2/orders/<id> and /wc/v3/orders/<id> no longer convert arbitrary non-shop_order posts into orders. If a migration depended on that side effect, fix the migration rather than working around it.

When to pick v4 over v3

Need Use
ID-list operations (include[]=1&include[]=2) on customers v4 (better-shaped query params)
DELETE a shipping zone or zone method v4 only
Fulfillments CRUD (tracking numbers, providers, lifecycle) v4 only — feature didn't exist in v3
Granular settings endpoints (per-group instead of all-in-one) v4
Payment gateway settings PUT with top-level fields v4 (v3's values wrapper less ergonomic)
Single shipping zone method by instance ID v4 only
Plain CRUD on products / orders / categories where v3 already works v3 is fine; v4 doesn't add value here
External integration written before WC 10.2 Stay on v3 unless you specifically need v4 features

v3 is not deprecated. WC has shipped v3 since 2018 and there's no announced sunset. Adopt v4 endpoints for what they add; don't migrate working v3 code wholesale "just because".

Authentication — same as v3

v4 inherits WP REST authentication. From the WC perspective:

  • Cookie + nonce (X-WP-Nonce header from wp_create_nonce('wp_rest')) for browser / admin context.
  • Basic Auth over HTTPS for server-to-server, with the WC-issued consumer key / secret pair as username / password.
  • Application Passwords (WP 5.6+) — works the same.
  • OAuth 1.0a — WC's classic external-integration scheme.

No new auth types in v4. The permission_callback enforcement is identical.

Customer-owned account endpoints

Do not confuse Woo's admin/integration REST API with customer-facing account actions. WC REST resources such as orders/subscriptions are generally protected by capabilities and consumer keys; they are not automatically safe to expose to a logged-in customer for "my account" operations.

When implementing custom routes for account actions:

  • Use register_rest_route() with a strict permission_callback.
  • Authenticate as a WP user and derive the user ID server-side with get_current_user_id().
  • Never ship Woo consumer keys, application passwords, or Stripe secret keys to a public client.
  • For every object ID in the request, verify ownership: order customer ID, subscription customer ID, payment token user ID, or membership user ID.
  • Return only customer-safe fields; do not mirror admin REST responses wholesale.
  • Prefer domain actions over raw CRUD: cancel subscription, set default payment method, delete payment token, create setup intent, confirm payment method.
  • Keep writes behind nonces/bearer tokens, rate limits, idempotency keys for payment-related operations, and audit notes/logs.

Important surfaces:

Need Existing surface Customer-facing status
Cart/checkout Store API (wc/store/v1) Designed for shopper checkout, session/cart oriented.
Saved payment methods Woo account form/query handlers + WC_Payment_Tokens No complete customer REST CRUD; wrap token APIs yourself.
Stripe card setup Stripe Gateway AJAX + SetupIntent helpers Browser/account flow exists; custom REST must verify SetupIntent/customer server-side.
Subscriptions CRUD WCS /wc/v3/subscriptions Admin/server API; customer routes should enforce ownership and allowed transitions.
Subscription switch WCS switcher cart/checkout flow No simple REST endpoint; wrap the switch flow, do not raw-update line items/meta.
Membership access Memberships objects/hooks Expose only current user's membership state unless admin.

For Subscriptions, the REST controller can set status, transition_status, dates, line items, payment_method, and payment_details, but its permissions are Woo capability based. For customer actions, call WCS object methods after ownership checks: $subscription->can_be_updated_to(), $subscription->update_status(), update_dates(), and WCS change-payment/switch services.

Hook prefix pattern (for filtering responses)

The v4 abstract controller's get_hook_prefix() (AbstractController.php:181-190) builds the prefix as:

return 'woocommerce_rest_api_v4_' . str_replace( '-', '_', $this->rest_base ) . '_';

Note: it ONLY replaces - with _, NOT /. So most routes get a clean prefix:

rest_base Hook prefix Example filter
customers woocommerce_rest_api_v4_customers_ ..._item_response
orders woocommerce_rest_api_v4_orders_ ..._collection_params
order-notes woocommerce_rest_api_v4_order_notes_ ..._item_schema
shipping-zones woocommerce_rest_api_v4_shipping_zones_ ..._item_response
shipping-zone-method woocommerce_rest_api_v4_shipping_zone_method_
fulfillments woocommerce_rest_api_v4_fulfillments_

The three predictable filter points per controller:

woocommerce_rest_api_v4_<base>_collection_params
woocommerce_rest_api_v4_<base>_item_schema
woocommerce_rest_api_v4_<base>_item_response

Example — modify the customers item response to include a custom field:

add_filter( 'woocommerce_rest_api_v4_customers_item_response', static function ( $response, $item, $request ) {
    $data = $response->get_data();
    $data['my_custom_field'] = (string) get_user_meta( $item->get_id(), '_my_field', true );
    $response->set_data( $data );
    return $response;
}, 10, 3 );

Caveat — sub-routes don't follow this pattern. The /wc/v4/fulfillments/providers sub-route uses a custom filter woocommerce_rest_prepare_fulfillments_providers (Fulfillments/Controller.php:463) — it's not part of the abstract's auto-generated three. When in doubt, grep the controller for apply_filters( to find what hooks it actually exposes.

Settings paths with / don't generate a prefix from the path. Settings/PaymentGateways/Controller.php has rest_base = 'payment-gateways' (set in the controller class), not 'settings/payment-gateways'. The /settings/ segment in the URL comes from how the route is REGISTERED ('/' . 'settings' . '/' . $this->rest_base), not from rest_base. So the hook prefix is woocommerce_rest_api_v4_payment_gateways_ — verify against the specific controller before relying on the prefix.

Standard error codes

The v4 AbstractController defines shared error codes (V4/AbstractController.php:34-40):

Code When
invalid_id id parameter doesn't match an existing resource
resource_exists POST collides with an existing resource
cannot_create POST failed at the data-store level
cannot_update PUT/PATCH failed at the data-store level
cannot_delete DELETE failed at the data-store level
cannot_trash DELETE without force=true failed
trash_not_supported DELETE without force=true on a non-trash-supporting resource

Error responses use a stable shape via get_route_error_response( $error_code, $error_message, $http_status, $additional_data ) — useful to handle on the client.

RestApiCache trait — WC 10.5+ caching

Automattic\WooCommerce\Internal\Traits\RestApiCache (src/Internal/Traits/RestApiCache.php:124) wraps endpoint callbacks with response caching:

  • Cache key includes route, query params, user context (if vary_by_user is set), and a version string.
  • Storage: wp_cache_* (object cache). On installs without an external object cache (Redis / Memcached), the trait falls back to per-request memory.
  • Feature-gated on feature_is_enabled('rest_api_caching') AND the option woocommerce_rest_api_enable_backend_caching = 'yes'.
  • TTL configurable per route via the with_cache( $callback, [ 'cache_ttl' => N ] ) config array.
  • Invalidation by entity-type tagging: changes to a tagged entity (e.g. 'order') bust the cached responses tied to it.

This trait is for WC's own controllers. Plugin-defined routes wouldn't use it (it's Internal\Traits); they'd use the WP-native cache primitives or roll their own. Knowing the feature exists is the value here — when you see X-WC-Cache headers in v4 responses, this is what's emitting them.

Calling v4 from PHP / JS

PHP — internal call (admin-context AJAX, REST plumbing):

$request  = new WP_REST_Request( 'GET', '/wc/v4/customers' );
$request->set_param( 'orderby', 'id' );
$request->set_param( 'order',   'desc' );
$request->set_param( 'per_page', 50 );
$response = rest_do_request( $request );

if ( ! $response->is_error() ) {
    $customers = $response->get_data();
}

JS — @wordpress/api-fetch from inside wp-admin:

import apiFetch from '@wordpress/api-fetch';

const customers = await apiFetch( {
    path: '/wc/v4/customers?orderby=id&order=desc&per_page=50',
} );
// X-WP-Nonce attached automatically inside admin context.

Server-to-server with Basic Auth:

curl -u "$CK:$CS" \
    "https://store.example/wp-json/wc/v4/orders?status=processing"

The endpoint surface is REST-conventional — same query params (per_page, orderby, order, include, exclude, status), same response envelope, same X-WP-Total / X-WP-TotalPages pagination headers.

Critical rules

  • wc/v4 is the namespace, NOT wc-blocks/v4 or wc-store/v4. Those are different APIs (Store API and Blocks API; not covered here).
  • v4 coexists with v3. Both work; pick per-endpoint based on what each provides.
  • Admin REST is not a My Account API. Do not expose WC consumer keys or capability-protected order/subscription endpoints directly to customers; write customer-owned routes with explicit ownership checks.
  • Don't extend Automattic\WooCommerce\Internal\RestApi\Routes\V4\AbstractController for your plugin's REST routes. The Internal\ namespace prefix is WC's "we may break this in any release" signal. Use WP_REST_Controller directly (see wp-rest-api skill).
  • Filter responses via woocommerce_rest_api_v4_<route>_item_response when you need to add fields without rewriting the route. Same pattern works in v3 with the v3-prefixed hook (woocommerce_rest_prepare_*).
  • DELETE on shipping zones is v4-only. v3 lacks it; if you're cleaning up zones programmatically, use v4.
  • Fulfillments only exist on v4. v3 has no equivalent. See sibling skill wc-shipping-providers for the related provider abstraction.
  • status=any is not "including checkout drafts" in WC 10.8+. Ask for status=checkout-draft explicitly if you are auditing draft checkout orders.
  • V4 products can omit sensitive fields depending on capabilities. Missing downloads, purchase_note, cost_of_goods_sold, or meta_data is expected for under-privileged users.
  • The RestApiCache trait is internal. Don't rely on its existence in plugin code; it's WC's mechanism for its own routes.
  • Authentication is unchanged from v3. Cookie + X-WP-Nonce, Basic Auth with WC API keys, Application Passwords, OAuth 1.0a — all the same.

Common mistakes

// WRONG — extending an Internal class for plugin routes
class MyController extends Automattic\WooCommerce\Internal\RestApi\Routes\V4\AbstractController { }
// The Internal\ namespace can be reshaped in any WC minor release.

// RIGHT — extend WP_REST_Controller directly
class MyController extends WP_REST_Controller { }

// WRONG — assuming v3 path is canonical
const URL = '/wp-json/wc/v3/customers'; // missing v4's better customer ordering

// RIGHT — pick the right version per use case
const URL = '/wp-json/wc/v4/customers?orderby=id&order=desc';

// WRONG — trying to DELETE a shipping zone via v3
fetch('/wp-json/wc/v3/shipping/zones/5', { method: 'DELETE' });
// 404 — v3 doesn't expose DELETE on this route

// RIGHT — v4 supports it (note dash-separated rest_base, NOT /shipping/zones/)
fetch('/wp-json/wc/v4/shipping-zones/5', { method: 'DELETE', headers: { 'X-WP-Nonce': nonce } });

// WRONG — hardcoding the v4 hook with the wrong prefix
add_filter( 'woocommerce_rest_v4_customers_item_response', $cb ); // wrong prefix

// RIGHT
add_filter( 'woocommerce_rest_api_v4_customers_item_response', $cb, 10, 3 );

// WRONG — assuming RestApiCache headers mean external cache is configured
header( 'X-WC-Cache: HIT' ); // your endpoint can't fake this for clients

// (RestApiCache is WC-internal — your plugin doesn't emit X-WC-Cache headers
//  unless you implement caching independently with the WP cache primitives.)

Cross-references

  • Run wp-rest-api for the broader WP REST API patterns — register_rest_route, permission_callback, args schema. Plugin-defined REST routes belong here, NOT under WC's v4 namespace.
  • Run wc-shipping-providers when working with the v4 fulfillments / providers endpoints — the AbstractShippingProvider class is the data shape behind those routes.
  • Run wc-hpos-compatibility when v4 order endpoints are involved — order data goes through HPOS by default in WC 10.x.

What this skill does NOT cover

  • The Store API (/wp-json/wc/store/v1/...) — different surface entirely, customer-facing checkout / cart APIs. See sibling skill wc-store-api.
  • The Blocks API (/wp-json/wc-blocks/) — also separate.
  • Migration tooling for moving v3 integrations to v4 — there isn't one; rewrite per-endpoint.
  • Schema-by-schema diff between v3 and v4 fields. Use OPTIONS /wc/v4/<resource> against a live install to fetch the JSON Schema; trust the live response over any documentation.
  • WC POS endpoints that may use bespoke namespaces. Out of scope.
  • WP REST authentication mechanics (cookie+nonce flow, Basic over HTTPS, OAuth) — handled by wp-rest-api and the official WC REST docs.

References