WC-CLI

This content was generated with the assistance of AI and should be verified against the official documentation of each platform before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

WC-CLI is the part of WooCommerce that adds the wp wc family of commands to WP-CLI, the command-line interface of WordPress. It lets a shell script, a deployment job or a developer at a terminal create products, place orders, run maintenance tools, check the High-Performance Order Storage (HPOS) tables and export or import store settings without a browser and without HTTP credentials. It has shipped with WooCommerce since 3.0.0; this page was written and run against WooCommerce 11.2.1 (published 2026-10-09) on WordPress 7.1.3 with WP-CLI 2.12.0, PHP 8.3.36 and MariaDB 11.8.9, and checked on 2026-10-27. It applies to any self-hosted WooCommerce store whose host lets you run wp. WP-CLI itself (installation, global parameters and the core commands) is covered in WP-CLI.

The thread of the page is the Pocket Coffee shop: a second way to create the catalogue that the REST API and Webhooks page built over HTTP, then a bulk price rise, a first order, the HPOS checks that follow it, and a Blueprint of the store settings. WC-CLI is, by design, the same code as the REST API, so reading that page first explains most of the behaviour here; the HPOS commands belong with HPOS: High-Performance Order Storage, the queue behind woocommerce_deliver_webhook_async with Action Scheduler, and a first installation with Installation and Local Environments. Shell scripting basics are on the Command-line PHP page and in the Docker material of this site (Docker).

How wp wc commands work: WP-CLI loads WordPress, WC_CLI_Runner turns the wc/v2 REST routes into commands, each command dispatches a REST request in the same process as the user given by the user flag, and the result is printed; a second group of commands such as hpos and blueprint is written by hand

How WC-CLI is built

There are two kinds of command, and the difference explains most surprises.

Commands generated from REST routes. When wp wc starts, the class WC_CLI_Runner creates an in-process WordPress REST server, lets every plugin register its routes, and walks the routes of the wc/v2 namespace. For every route that has a JSON Schema title it registers a WP-CLI command named after that title: wp wc product, wp wc shop_order, wp wc product_variation and so on, with up to five subcommands (list, get, create, update, delete) depending on the methods the route supports, and one option for every argument of the route. Batch routes are skipped, and so are the routes settings, system_status (with its tools) and reports, because they are better served by wp option, wp wc tool and the admin. Running a command builds a WP_REST_Request, defines REST_REQUEST, and dispatches it with rest_do_request() inside the same PHP process: there is no HTTP request, no consumer key and no OAuth signature. The controller code, the permission checks, the validation and the hooks are exactly those of the REST API.

Three consequences are worth stating plainly:

  • The identity of a command is the --user global flag of WP-CLI. Without it the request is anonymous and the controller answers 401; WC-CLI adds a hint to the message (below).

  • The version is wc/v2, not wc/v3. The field names are the v2 ones (in_stock, not stock_status), the help text lists the v2 arguments, and --format=headers shows Link URLs of /wp-json/wc/v2/.... Behaviour that only the v3 controllers have is missing; the clearest example found on 11.2.1 is that coupon_lines of an order are applied as discounts by wc/v3 but only stored by wc/v2.

  • Anything that extends the REST API extends WC-CLI too. The roast_date field that the REST page added with register_rest_field() appeared in wp wc product get 22 --field=roast_date (it printed 2026-10-27) without any CLI code.

Hand-written commands. Some groups do not use REST at all: hpos (and its deprecated alias cot), blueprint, palt (product attributes lookup table), tool, update, tracker and com. They are PHP classes that call WP-CLI’s API directly, so they have their own options and output.

The official documentation page for wp wc lists the groups and says to check availability with wp wc --info; on 11.2.1 that command prints the usage of the wp wc namespace (the list of groups), which serves the same purpose.

Running it

WP-CLI is installed once per server (see the WP-CLI site) and is run from the WordPress directory or with --path. Where PHP is not installed on the host, the equivalent is a short-lived container of the official wordpress:cli image attached to the network and volumes of the site, which is how every command on this page was run (the database settings are passed as environment variables, shown here as placeholders):

#!/bin/sh
# wp.sh - run WP-CLI against the local WordPress container (usage: ./wp.sh wc product list --user=admin)
exec docker run --rm --network ec-wp --volumes-from ec-wordpress -u 33:33 \
  -e WORDPRESS_DB_HOST=ec-wordpress-db -e WORDPRESS_DB_USER=wpuser \
  -e WORDPRESS_DB_PASSWORD="${DB_PASSWORD}" -e WORDPRESS_DB_NAME=wordpress \
  -e HOME=/tmp wordpress:cli-php8.3 wp --path=/var/www/html "$@"

In the examples below the command is written wp ... and the administrator is --user=admin; on the test store the administrator is called admin, and wp stood for the wrapper above. Help is built in and is generated from the REST schema, so it always matches the installed version:

wp wc                         # every group
wp wc product                 # the subcommands of a group
wp help wc product list       # all options of one command (also: wp wc product list --help)

Without --user, a REST-derived command stops with the reminder that the 401 comes from a missing user:

$ wp wc product list --format=count
Error: Sorry, you cannot list resources.
Make sure to include the --user flag with an account that has permissions for this action. {"status":401}

The user can be an id, a login or an email address, and it must have the capability the REST controller checks (manage_woocommerce and the post capabilities of the resource; an Administrator or a Shop manager works). A command that should run as a service identity is best run as a dedicated Shop manager user.

The command groups

The groups registered on 11.2.1 (the list is what wp wc printed):

Group Kind Use

product, product_variation, product_attribute, product_attribute_term, product_cat, product_tag, product_brand, product_shipping_class, product_review

REST (wc/v2)

Catalogue: products with their variations, attributes, terms, categories, tags, brands, shipping classes and reviews

shop_order, shop_order_refund, order_note

REST

Orders, refunds and order notes (the order group is shop_order; there is no wp wc order)

customer, customer_download, shop_coupon

REST

Customers, their downloads, and coupons

shipping_zone, shipping_zone_location, shipping_zone_method, shipping_method, payment_gateway, tax, tax_class

REST

Shipping zones and methods, payment gateways, tax rates and classes (shipping_zone_location has no create; use update with the zone id)

webhook, webhook_delivery

REST

Webhooks. The webhook_delivery group is registered, but webhook_delivery list answered No route was found matching the URL and request method on 11.2.1; deliveries are written to the WooCommerce log (source webhooks-delivery)

hpos (alias cot, deprecated)

Hand-written

HPOS status, sync and repair; see below

blueprint

Hand-written

Export and import of store settings; see below

tool, update, palt, tracker, com

Hand-written

System tools (tool list, tool run <id>), pending database updates, the product attributes lookup table, a usage-tracking snapshot, and the WooCommerce.com connection (com connect, com disconnect, com extension)

Two corrections to commonly repeated advice. There is no wp wc setting or wp wc settings group (the official command index and FAQ still mention wp wc setting update): store settings are options, so use wp option update, or the /wc/v3/settings REST routes. And the first thing a new store needs, its setup wizard, is a browser flow that no wp wc command runs; the general settings are set with options, as the installation page shows.

Conventions of the generated commands

Flag or argument Behaviour

--user=<id|login|email>

Required. The WordPress user the command acts as.

Positional ids

wp wc product get 22, wp wc product_variation list <product>, wp wc order_note create <order>, wp wc product_variation update <product> <variation>: the ids that appear in the route come first, in route order.

--fields=a,b,c, --field=name

Limit a list or get to some properties, or print the value of one property. The names are those of the v2 schema: --fields=id,stock_status failed with Invalid field: stock_status (the v2 name is in_stock).

--format=<format>

table (default), json, csv, yaml, ids, count, and for debugging headers, body and envelope. The table is tab-separated when the output is not a terminal. count prints the total without a newline.

--porcelain

On create, update and delete: print only the id. This is what scripts capture.

--force=true

delete moves the item to the trash by default and prints Trashed; --force=true removes it for good and prints Deleted. The flag needs a value: a bare --force printed Warning: --force parameter needs a value and only trashed the product.

Lists and objects

Pass JSON: --categories='[{"id":25}]', --line_items='[{"product_id":37,"quantity":2}]', --billing='{"country":"ES"}'. A value that starts with [ or { is decoded as JSON.

Filters

The arguments of the REST collection: --sku, --status, --search, --per_page, --page, --category, --customer, --after, --modified_after, …​ The help of each command lists them.

--debug=wc

Prints the number of queries the command ran and, ordered by duration, the slowest queries (Debug: wc command executed 11 queries in 0.001297 seconds).

Exit codes follow WP-CLI: 0 on success and 1 on an error, with the message on standard error, so set -e scripts stop at the first failure (wp wc product get 999999 printed Error: Invalid ID. {"status":404} and returned 1).

Pocket Coffee from the command line

Products

The commands below create, read, update and delete the Pocket Coffee mug (SKU PC-MUG-CLI); --porcelain returns the id that the next commands use.

ID=$(wp wc product create --name="Pocket Mug" --type=simple --regular_price=9.50 \
       --sku=PC-MUG-CLI --manage_stock=true --stock_quantity=25 --user=admin --porcelain)
wp wc product list --sku=PC-MUG-CLI --fields=id,name,sku,price,stock_quantity --user=admin
wp wc product update "$ID" --regular_price=10.00 --user=admin --porcelain
wp wc product get "$ID" --field=price --user=admin
wp wc product list --sku=PC-MUG-CLI --fields=id,sku,price --format=csv --user=admin
wp wc product list --format=count --user=admin
wp wc product delete "$ID" --force=true --user=admin
id	name	sku	price	stock_quantity
197	Pocket Mug	PC-MUG-CLI	9.50	25
197
10.00
id,sku,price
197,PC-MUG-CLI,10.00
32
Success: Deleted product 197.

The 197 after the table is what update --porcelain printed (the first --porcelain call stored the id in the shell variable instead of printing it), and the 32 is the count of products at that moment (printed without a newline). A --format=ids list is the usual input of loops:

wp wc product list --sku=PC-KIT --format=ids --user=admin
wp wc product list --sku=PC-KIT --fields=id,sku,price --format=yaml --user=admin
22
---
-
  id: 22
  sku: PC-KIT
  price: "24.00"

A variable product, its variations and a bulk change

A variable product is created with its attributes as JSON, and each variation is a separate product_variation command that takes the parent id first:

PID=$(wp wc product create --name="Single-origin beans (CLI)" --type=variable --sku=PC-BEANS-CLI \
        --categories='[{"id":25}]' --user=admin --porcelain \
        --attributes='[{"name":"Grind","visible":true,"variation":true,"options":["Whole bean","Espresso"]},
                       {"name":"Size","visible":true,"variation":true,"options":["250 g","1 kg"]}]')

wp wc product_variation create "$PID" --sku=PC-ETH-ESP-250-CLI --regular_price=9.90 \
   --manage_stock=true --stock_quantity=40 --user=admin --porcelain \
   --attributes='[{"name":"Grind","option":"Espresso"},{"name":"Size","option":"250 g"}]'

wp wc product_variation list "$PID" --fields=id,sku,price,attributes --format=json --user=admin
[{"id":206,"sku":"PC-ETH-ESP-250-CLI","price":"9.90","attributes":[{"id":0,"name":"Grind","slug":"grind","option":"Espresso"},{"id":0,"name":"Size","slug":"size","option":"250 g"}]}]

(On the test store the variable product got id 205 and this first variation 206; the JSON above is the entry of variation 206 in a list that, at that time, held only that variation. The other three variations were created the same way.) A bulk change is a loop over a csv list: raise every kilo bag by 1.00 EUR, leaving the small bags alone.

wp wc product_variation list "$PID" --fields=id,sku,regular_price --format=csv --user=admin \
  | tail -n +2 \
  | while IFS=, read -r id sku price; do
      case "$sku" in
        *-1KG-*) wp wc product_variation update "$PID" "$id" \
                   --regular_price="$(awk -v p="$price" 'BEGIN { printf "%.2f", p + 1 }')" \
                   --user=admin --porcelain ;;
      esac
    done
wp wc product_variation list "$PID" --fields=id,sku,regular_price --user=admin
376
375
id	sku	regular_price
376	PC-ETH-ESP-1KG-CLI	30.90
375	PC-ETH-WHL-1KG-CLI	30.90
373	PC-ETH-WHL-250-CLI	8.90
206	PC-ETH-ESP-250-CLI	9.90

For the 18-variation product of the REST page, the variations/generate and variations/batch calls of REST are the better tool (one request instead of eighteen process starts); WC-CLI skips batch routes, so a loop is the CLI way. Every wp start loads WordPress and all plugins, which costs a few hundred milliseconds, and a loop of a thousand updates is therefore minutes; for large changes prefer one wp eval-file script that uses the CRUD objects, or the REST batch.

Customers, orders, notes and refunds

CID=$(wp wc customer create --email=grace.cli@example.test --first_name=Grace --last_name=Hopper \
        --user=admin --porcelain)

OID=$(wp wc shop_order create --status=processing --customer_id="$CID" --payment_method=bacs \
        --billing='{"first_name":"Grace","last_name":"Hopper","email":"grace.cli@example.test","country":"DE","city":"Berlin","postcode":"10115","address_1":"Unter den Linden 1"}' \
        --line_items="[{\"product_id\":${PID},\"variation_id\":206,\"quantity\":2}]" \
        --shipping_lines='[{"method_id":"flat_rate","method_title":"Flat rate","total":"9.90"}]' \
        --user=admin --porcelain)

wp wc shop_order get "$OID" --fields=id,status,total,currency,payment_method --user=admin
wp wc order_note create "$OID" --note="Packed on 2026-10-27" --user=admin --porcelain
wp wc shop_order update "$OID" --status=completed --user=admin --porcelain
wp wc shop_order list --status=completed --customer="$CID" --fields=id,status,total --user=admin
wp wc shop_order_refund create "$OID" --amount=5.00 --reason="Late delivery" --api_refund=false \
   --user=admin --porcelain
wp wc shop_order_refund list "$OID" --fields=id,amount,reason --user=admin
Field	Value
id	207
status	processing
currency	EUR
total	29.70
payment_method	bacs
195
207
id	status	total
207	completed	29.70
210
id	amount	reason
210	5.00	Late delivery

The Field Value table is how get prints one item. The order total is 29.70 (two bags at 9.90 plus 9.90 shipping); the note and the refund returned their own ids (195 and 210), update returned the order id, and --api_refund=false records the refund without asking the gateway, which is required for bank transfer exactly as on the REST API. The orders landed in the HPOS table, as the section below shows. The test container has no mail transport, so each state change also printed sendmail: can't connect to remote host (127.0.0.1) on standard error while the commands still succeeded.

A complete seed script

Every command above is combined in one script that builds the Pocket Coffee demo data from scratch: a category, a variable product with four variations, a coupon, a customer and a first order. It needs bash and WP_USER (the administrator); WP defaults to wp.

#!/usr/bin/env bash
# Seeds a Pocket Coffee category, a variable product with four variations, a coupon,
# a customer and a first order with WC-CLI. Run it from the WordPress directory, or set WP to a wrapper.
set -euo pipefail

WP="${WP:-wp}"
ACT="--user=${WP_USER:-admin}"

category=$($WP wc product_cat create --name="Beans (CLI seed)" "$ACT" --porcelain)

product=$($WP wc product create \
  --name="Single-origin beans (seed)" --type=variable --sku=PC-SEED-BEANS \
  --categories="[{\"id\":${category}}]" \
  --attributes='[{"name":"Grind","visible":true,"variation":true,"options":["Whole bean","Espresso"]},
                 {"name":"Size","visible":true,"variation":true,"options":["250 g","1 kg"]}]' \
  "$ACT" --porcelain)

kilo_bag=""
for row in "Whole bean|250 g|WHL-250|8.90" "Whole bean|1 kg|WHL-1KG|29.90" \
           "Espresso|250 g|ESP-250|8.90" "Espresso|1 kg|ESP-1KG|29.90"; do
  IFS='|' read -r grind size code price <<<"$row"
  id=$($WP wc product_variation create "$product" \
    --sku="PC-SEED-$code" --regular_price="$price" --manage_stock=true --stock_quantity=40 \
    --attributes="[{\"name\":\"Grind\",\"option\":\"${grind}\"},{\"name\":\"Size\",\"option\":\"${size}\"}]" \
    "$ACT" --porcelain)
  if [ "$code" = "WHL-1KG" ]; then kilo_bag=$id; fi
done

$WP wc shop_coupon create --code=coffeeclub-seed --discount_type=percent --amount=10 \
  --product_categories="[${category}]" "$ACT" --porcelain >/dev/null

customer=$($WP wc customer create --email=ada.seed@example.test --first_name=Ada --last_name=Lovelace "$ACT" --porcelain)

order=$($WP wc shop_order create --status=processing --customer_id="$customer" --payment_method=bacs \
  --billing='{"first_name":"Ada","last_name":"Lovelace","address_1":"Calle de Alcala 1","city":"Madrid","postcode":"28014","country":"ES","state":"M","email":"ada.seed@example.test"}' \
  --line_items="[{\"product_id\":${product},\"variation_id\":${kilo_bag},\"quantity\":2}]" \
  --shipping_lines='[{"method_id":"flat_rate","method_title":"Flat rate","total":"4.90"}]' \
  "$ACT" --porcelain)

echo "category=$category product=$product customer=$customer order=$order"
$WP wc shop_order get "$order" --fields=id,status,discount_total,total,currency "$ACT"
category=36 product=397 customer=11 order=403
Field	Value
id	403
status	processing
currency	EUR
discount_total	0.00
total	64.70

The total is 2 bags at 29.90 plus 4.90 of shipping. The discount_total is 0.00 on purpose: the script does not send --coupon_lines. Sending --coupon_lines='[{"code":"coffeeclub-seed"}]' created a coupon line with a discount of 0.00 (on create, and on a later update, which added a second line), because the wc/v2 controller behind WC-CLI stores a coupon line as given and does not calculate it; the same request through PUT /wc/v3/orders/<id> calculated 5.98 of discount and a total of 58.72. For discounted orders use the REST v3 API, or pass the discount amounts yourself. The Coffee Club rule of the running example (10 percent on beans for members) needs that calculation, which is why the page that covers it, Taxes, Coupons, Currencies and i18n, builds it with the coupon engine.

Webhooks, gateways and the rest

wp wc webhook list --fields=id,name,status,topic --user=admin
WH=$(wp wc webhook create --name="Pocket Coffee cli" --topic=order.updated \
       --delivery_url=https://roastery.example.test/hooks --secret="${WC_WEBHOOK_SECRET}" \
       --status=paused --user=admin --porcelain)
wp wc webhook update "$WH" --status=active --user=admin --porcelain
wp wc webhook get "$WH" --fields=id,topic,status,hooks --format=json --user=admin
wp wc webhook delete "$WH" --force=true --user=admin
id	name	status	topic
3	Pocket Coffee failing demo	disabled	product.updated
1	Pocket Coffee order placed	active	order.created
2	Pocket Coffee batch roasted	active	action.woocommerce_pocket_coffee_batch_roasted
4
{"id":4,"status":"active","topic":"order.updated","hooks":["woocommerce_update_order","woocommerce_order_refunded"]}
Success: Deleted webhook 4.

(The first listing shows the webhooks of the REST page that already existed on the store, among them one that was disabled by repeated failures; the test delivery URL of the new webhook was not contacted, the commands only store it. The id 4 after the listing is what update --porcelain printed; the one from create went into WH. The delivery URL is on a reserved domain, unlike the webhooks of the REST page, which pointed at a receiver on the Docker network.) Other generated commands read and change configuration:

wp wc payment_gateway get bacs --fields=id,title,enabled --user=admin
wp wc tax_class list --fields=slug,name --user=admin
wp wc product_cat list --fields=id,name,slug,count --user=admin --per_page=3
Field	Value
id	bacs
title	Direct bank transfer
enabled	true
slug	name
standard	Standard rate
reduced-rate	Reduced rate
zero-rate	Zero rate
id	name	slug	count
32	Beans	beans	1
36	Beans (CLI seed)	beans-cli-seed	1
25	Beans (REST demo)	beans-rest-demo	2

wp wc payment_gateway update bacs --enabled=true is the way to switch bacs, cheque and cod on or off from a script (it was run once on the test store while it was set up; cod additionally needs a shipping method that supports it). wp wc shipping_zone list and shipping_zone_method list <zone> read the zones; the latter printed PHP warnings of the type Undefined array key from the v2 controller on this store, whose zones carried class-cost settings from other tests, while still returning the rows: warnings on standard error do not change the exit code, so a script that treats any output on standard error as a failure needs to be tolerant, or run the command through the REST API instead.

Maintenance commands

wp wc update
wp wc tool list --fields=id,name,action --user=admin
Success: No updates required. Database version is 11.2.1
id	name	action
clear_transients	WooCommerce transients	Clear transients
clear_expired_transients	Expired transients	Clear transients
delete_orphaned_variations	Orphaned variations	Delete orphaned variations
clear_expired_download_permissions	Used-up download permissions	Clean up download permissions
regenerate_product_lookup_tables	Product lookup tables	Regenerate
...
  • wp wc update runs the pending WooCommerce database updates (the same as the "Update database" button after an upgrade); on a store that is up to date it says so. In a deployment script it belongs right after the plugin files are replaced and before traffic is switched back.

  • wp wc tool list shows the system tools of WooCommerce > Status > Tools, and wp wc tool run <id> runs one. The ids on 11.2.1 are clear_transients, clear_expired_transients, delete_orphaned_variations, clear_expired_download_permissions, regenerate_product_lookup_tables, repair_coupons_lookup_table, recount_terms, reset_roles, clear_sessions, clear_template_cache, clear_system_status_theme_info_cache, install_pages, delete_taxes, regenerate_thumbnails, db_update_routine, recreate_order_address_fts_index, verify_db_tables, regenerate_product_attributes_lookup_table, abort_product_attributes_lookup_table_regeneration, clear_woocommerce_analytics_cache, refresh_remote_inbox_notifications, delete_inbox_notification, regenerate_order_fulfillment_status, hpos_legacy_cleanup, delete_custom_orders_table, schedule_expired_transient_files_cleanup and rebuild_analytics_tax_data. Several are destructive (delete_taxes, reset_roles, delete_custom_orders_table, hpos_legacy_cleanup): read the name twice. The list was printed; tool run was not run on the shared test store.

  • wp wc palt manages the product attributes lookup table that speeds up attribute filtering: info, enable, disable, regenerate (immediate), initiate_regeneration, abort_regeneration, resume_regeneration, cleanup_regeneration_progress and regenerate_for_product <id>. wp wc palt info printed the table name, whether its use is enabled and the row count (and, while another test was regenerating it, a warning that a regeneration was in progress).

  • wp wc tracker snapshot dumps, to the screen, the usage data WooCommerce sends when usage tracking is on (site, theme, plugins, counts and gateways): read it before you accept tracking on a store with privacy requirements. It was run and its output (which includes the site URL and plugin lists) is not reproduced here.

  • wp wc com connect, disconnect and extension manage the WooCommerce.com connection and Marketplace extensions with an application password. They need a Woo.com account and were not run.

HPOS from the command line

The wp wc hpos commands move and check orders between the classic posts tables and the HPOS tables (wc_orders, wc_order_addresses, wc_order_operational_data, wc_orders_meta). They are hand-written, need no --user, and have been the supported interface since the namespace was renamed from wp wc cot in 8.9.0; the old name still works as a deprecated alias.

The wp wc hpos commands: enable and disable switch between posts authoritative and HPOS authoritative, compatibility mode and sync keep both stores written, and status, verify_data, diff, backfill and cleanup inspect and repair them
Command What it does

wp wc hpos status

Summary: whether HPOS and compatibility mode are enabled, the unsynced orders and the orders subject to cleanup.

wp wc hpos enable [--for-new-shop] [--with-sync] [--ignore-plugin-compatibility]

Makes HPOS authoritative if the stores are in sync, or if the shop is new. Checks that active plugins are compatible; --with-sync also turns compatibility mode on.

wp wc hpos disable [--with-sync]

Makes the posts tables authoritative again, if no orders are pending sync; --with-sync also turns compatibility mode off.

wp wc hpos compatibility-mode enable, disable

Turns the two-way write (sync) on or off.

wp wc hpos count_unmigrated

Number of orders pending sync.

wp wc hpos sync [--batch-size=500]

Copies orders from the authoritative store to the other one, in batches.

wp wc hpos verify_data [--batch-size=500] [--start-from=<id>] [--end-at=<id>] [--verbose] [--order-types=<types>] [--re-migrate]

Compares the two stores for all orders and lists the differing columns. --re-migrate overwrites orders that fail verification, so use it only on a site that never ran with HPOS as the source of truth.

wp wc hpos diff <order_id> [--format=table|csv|json|yaml]

Shows the property differences of one order between the stores. It does not repair anything.

wp wc hpos backfill <order_id> --from=<hpos|posts> --to=<hpos|posts> [--meta_keys=...] [--props=...]

Copies a whole order or only some properties and meta keys from one store to the other, regardless of the setting.

wp wc hpos cleanup <all|id|range>... [--batch-size=500] [--force]

Deletes redundant postmeta of migrated orders once HPOS is authoritative and compatibility mode is off. Destructive; stops when the post looks newer than the HPOS row unless --force. Placeholder posts of type shop_order_placehold stay.

wp wc hpos compatibility-info [--include-inactive] [--display-filenames]

Lists active plugins that declared, did not declare or contradict HPOS compatibility. Inactive plugins are counted as "uncertain".

The history of the commands, from the changelog: cot migrate was replaced by cot sync in 7.1.0 (2022-11-08), hpos diff arrived in 8.6.0 (2024-02-13), hpos backfill in 8.7.0 (2024-03-01), the cot namespace was deprecated and aliased to hpos in 8.9.0 (2024-05-14), cleanup learned to remove the metadata of deleted orders in 9.0.0 (2024-06-18), and the compatibility-mode enable and disable commands arrived in 9.1.0 (2024-07-10).

What was run on the test store, whose HPOS is enabled and authoritative, with compatibility mode (sync) off:

$ wp wc hpos status
HPOS enabled?: yes
Compatibility mode enabled?: no
Unsynced orders: 80
Orders subject to cleanup: 7

$ wp wc hpos count_unmigrated
There are 80 orders to be synced.

$ wp wc hpos diff 207
Warning: Differences found for order 207:
property	hpos	post
status	completed	draft
cart_tax	0
total	29.70	0.00
customer_id	6	0
billing_email	grace.cli@example.test
date_modified	2026-10-27T18:47:13+00:00	2026-10-27T18:47:07+00:00
payment_method	bacs
billing_first_name	Grace
billing_last_name	Hopper
billing_address_1	Unter den Linden 1
...

The numbers explain the model. The orders created in this section by wp wc shop_order create exist in full only in wc_orders; their posts rows are placeholders in draft status with a zero total, which diff shows column by column. With sync off that is correct and expected: HPOS is the source of truth and the posts side is not maintained, which is exactly why code must never read orders with get_post() or WP_Query. verify_data on this store reported Error: 6 orders were verified in 0 seconds. 1 error found followed by the JSON of the columns that differed for an order that another test had written only to the HPOS table; on a store that migrated with sync, a clean run ends with Success: <n> orders were verified in <s> seconds.

wp wc hpos compatibility-info printed 14 compatible plugins found: and the plugin names; a plugin that does not declare compatibility appears under "uncertain" and blocks enable unless --ignore-plugin-compatibility is passed. Compatibility is declared in code with FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ), as the plugins on the REST page and in the Extension Development page do.

The state-changing commands (enable, disable, compatibility-mode, sync, backfill, cleanup, verify_data --re-migrate) were not run, because the test store is shared and its HPOS settings must not change; their behaviour and outputs in this page come from the help text of 11.2.1 and the official CLI tools page. A migration of an existing store that follows those documents looks like this (to rehearse on a staging copy first, with a database backup):

wp wc hpos compatibility-info            # every active plugin declared compatible?
wp wc hpos compatibility-mode enable     # new writes go to both stores
wp wc hpos sync --batch-size=500         # copy the existing orders
wp wc hpos verify_data                   # expect: Success: N orders were verified
wp wc hpos enable                        # HPOS becomes authoritative
# ... run for a while with compatibility mode on, then, once everything is verified:
wp wc hpos compatibility-mode disable
wp wc hpos cleanup all --batch-size=500  # optional and destructive: removes redundant postmeta

For a brand-new store the single command is wp wc hpos enable --for-new-shop; a scripted install is not enabled automatically (the REST and installation pages show wp wc hpos status reporting HPOS enabled?: no right after a wp plugin install woocommerce --activate).

Blueprints

A Blueprint is a JSON file that describes the settings of a WooCommerce store so that it can be rebuilt elsewhere: plugins and themes to install, WooCommerce options to set and a few table rows. It is the WooCommerce counterpart of WordPress Playground blueprints and uses a similar steps array. The admin has an export and import screen under WooCommerce > Settings > Advanced > Blueprint (the blueprint feature has been on by default since 9.9.0); the command line has two commands:

wp wc blueprint export <save-to> [--steps=<steps>]
wp wc blueprint import <schema-path> [--show-messages=<all|error|info|debug>] [--yes]

Export

--steps names the exporters to run. The names are the aliases of the exporters: installPlugin, installTheme, setWCSettingsGeneral, setWCSettingsProducts, setWCSettingsTax, setWCShipping, setWCSettingsAccount, setWCSettingsEmails, setWCSettingsIntegrations, setWCSettingsSiteVisibility and setWCSettingsAdvanced. Without the option everything is exported (28 steps on the test store). The shop’s general settings are a small file:

wp wc blueprint export pocket-coffee-general.json --steps=setWCSettingsGeneral --user=admin
{
    "landingPage": "\/wp-admin\/admin.php?page=wc-admin",
    "steps": [
        {
            "step": "setSiteOptions",
            "options": {
                "woocommerce_store_address": "",
                "woocommerce_store_address_2": "",
                "woocommerce_store_city": "",
                "woocommerce_default_country": "ES:M",
                "woocommerce_store_postcode": "",
                "woocommerce_allowed_countries": "all",
                "woocommerce_all_except_countries": "",
                "woocommerce_specific_allowed_countries": "",
                "woocommerce_ship_to_countries": "",
                "woocommerce_specific_ship_to_countries": "",
                "woocommerce_default_customer_address": "base",
                "woocommerce_address_autocomplete_enabled": "no",
                "woocommerce_calc_taxes": "yes",
                "woocommerce_enable_coupons": "yes",
                "woocommerce_calc_discounts_sequentially": "no",
                "woocommerce_currency": "EUR",
                "woocommerce_currency_pos": "left",
                "woocommerce_price_thousand_sep": ",",
                "woocommerce_price_decimal_sep": ".",
                "woocommerce_price_num_decimals": "2"
            }
        }
    ]
}

The file contains steps. The step types that the importer knows are installPlugin, installTheme, activatePlugin, activateTheme, setSiteOptions and runSql. The full export also contains runSql steps for the tables that hold settings (tax rates and classes, shipping zones and methods, pickup locations); their SQL uses the placeholder {WC_BLUEPRINT_TABLE_PREFIX} instead of wp_ since 11.0.0, so a Blueprint can be imported into a site with another table prefix. A tax class step, as exported, looks like this (the runSql step carries it as a literal resource named schema.sql):

replace into `{WC_BLUEPRINT_TABLE_PREFIX}wc_tax_rate_classes` (`tax_rate_class_id`, `name`, `slug`) VALUES ('1', 'Reduced rate', 'reduced-rate');

Payment gateway settings are excluded from exports since 11.2.0 (they must be configured by hand after an import), and the install steps list WordPress.org plugins and (since 10.1.0) only WordPress.org themes.

Treat an export as sensitive: the full file of the test store contained option groups such as the email sender and the MaxMind geolocation settings (which can hold a licence key). Read it before it leaves the machine, and do not commit it blindly.

Import

A Blueprint can be hand-written. This one sets the shop’s country and currency and a marker option of Pocket Coffee:

{
  "landingPage": "/wp-admin/admin.php?page=wc-settings",
  "steps": [
    {
      "step": "setSiteOptions",
      "options": {
        "pocket_coffee_blueprint_demo": "imported",
        "woocommerce_default_country": "ES:M",
        "woocommerce_currency": "EUR"
      }
    }
  ]
}
wp wc blueprint import pocket-coffee-blueprint.json --yes --show-messages=all --user=admin
Warning: A Blueprint imported with WP-CLI can change anything this command can access on the site — including data that is not described in the file. Only import files from a source you trust.
Step Processor	Type	Message
setSiteOptions	info	pocket_coffee_blueprint_demo has been updated.
setSiteOptions	info	woocommerce_default_country has not been updated because the current value is already up to date.
setSiteOptions	info	woocommerce_currency has not been updated because the current value is already up to date.
Success: pocket-coffee-blueprint.json imported successfully

The marker option was deleted again afterwards with wp option delete. An unknown step stops the import and --show-messages=all says why:

Error: Failed to import pc-wccli-bad.json. Run with --show-messages=all to debug
Step Processor	Type	Message
setSiteOptionz	error	Unable to find an importer

Safety rules, all documented in the changelog: the importer lists what the file will do before running it (including the number of database queries and the plugins and themes it installs or activates, 11.1.0), the options that must never be overwritten (for example the roles option wp_user_roles) are on a restricted list that is matched regardless of casing and accents (11.1.0), importing on a multisite network needs a network super administrator (11.2.0), and the UI import is restricted to "coming soon" mode (9.9.0), a restriction that the command-line import on the live-mode test store did not apply. --yes skips the confirmation, which makes Blueprints usable in provisioning scripts and CI, and also means the file is trusted blindly: pin it in version control and review changes like code. A Blueprint does not carry products, orders or customers, only settings and the tables listed above; moving content is a job for the export and import tools, REST or a database copy.

Scripting and automation patterns

  • Capture ids with --porcelain, loop over --format=ids, and quote the variables. A failed command returns 1, so set -euo pipefail stops a script at the first error, which also stops a pipe at the producer that failed.

  • Choose the identity once: WP_USER in the seed script, or wp --user=<id> at the top of a wrapper function. Do not run as an Administrator what a Shop manager can do.

  • Mind the plugin load. wp --skip-plugins also skips WooCommerce, so wp wc disappears; --skip-plugins=<other> skips only the named plugin. --skip-themes is safe: 11.1.0 and 11.2.0 fixed the cases where a WP-CLI run that skipped the theme consumed the queued rewrite flush and dropped the theme’s rewrite rules.

  • Mind the process cost. Each wp call boots WordPress; for thousands of items, call REST batch routes or one wp eval-file that uses CRUD objects (wc_get_products(), wc_get_orders(), the WC_Product and WC_Order setters). Never loop over orders with get_post_meta(): with HPOS the data is not in postmeta.

  • Schedule with the system cron, not with WP-Cron, when you need exact times: wp cron event run --due-now and wp action-scheduler run (see Action Scheduler) in a cron job give predictable queues, and on a deployment wp wc update followed by wp wc hpos status is a cheap health check.

  • Debug with --debug=wc to count queries per command, and with --format=headers or --format=envelope to see what the underlying REST call returned.

  • Do not run wp as root on a server; use the site’s user so that files that WooCommerce creates (logs, uploads) keep the right owner.

Troubleshooting

Symptom Cause and fix

Error: Sorry, you cannot list resources with the --user hint

The command ran anonymously. Add --user=<id|login|email> with an account that has the capability.

'product list' is not a registered subcommand of 'wc'

The argument order is wp wc <group> <command>: wp wc product list, not wp wc list product. Also check that WooCommerce is active (wp plugin list) and that --skip-plugins was not given.

Invalid field: stock_status

--fields uses the wc/v2 names (in_stock). Print wp wc product get <id> --format=json to see the field names.

Delete printed Trashed

The flag --force needs a value: --force=true.

A list or object option is ignored or rejected

Quote the JSON with single quotes and double quotes inside; use a double-quoted shell string with escaped inner quotes only when a shell variable has to be inserted.

wp wc order or wp wc setting not found

They do not exist: use shop_order, and options or REST settings.

wp wc hpos enable refuses

There are unsynced orders or an incompatible plugin: run wp wc hpos sync, read wp wc hpos compatibility-info, and only then consider --ignore-plugin-compatibility.

wp wc hpos disable refuses with "orders pending sync"

Run wp wc hpos sync first so that the posts tables hold the current data.

Warnings such as Undefined array key on standard error

PHP notices from an extension or from a v2 controller reading a setting that was never saved; they do not change the result. Fix the data or the extension, or silence them in wp-config.php for CLI runs.

What was run and what was not

  • Run on WooCommerce 11.2.1 with WP-CLI 2.12.0: wp wc and wp help wc product list; the 401 hint without --user; product create, list, update, get, csv, yaml, ids, count and delete (including the --force quirk); a variable product, four variations and the bulk loop; a customer, an order, an order note, a status change and a manual refund; the complete seed script; webhook create, update, get, list and delete; payment_gateway get, shipping_zone list, tax_class list and product_cat list; --format=headers and --debug=wc; the roast_date field in wp wc product get; wp wc update, wp wc tool list and wp wc palt info; wp wc hpos status, count_unmigrated, diff, verify_data and compatibility-info; Blueprint export (whole store, setWCSettingsGeneral, setWCSettingsTax with installPlugin) and import (a valid file and an invalid step, plus a missing file: Error: Invalid schema path).

  • Not run: wp wc hpos enable, disable, sync, backfill, cleanup, compatibility-mode and verify_data --re-migrate (they change the shared store’s order storage), wp wc tool run (global effects), wp wc com ... (needs a Woo.com account), the admin Blueprint screen, and the import of a full store Blueprint with plugin and theme steps (it would install software on the shared test store).

  • Documentation tensions to know about: the official command index and FAQ list wp wc setting although no such group exists on 11.2.1; the overview page says WC-CLI "leverages the capabilities of the WC REST API" without saying that it is the v2 API; the FAQ’s wp wc product delete 123 --force form does not delete (use --force=true); the HPOS CLI page documents diff formats list and json while the help lists table, csv, json and yaml.