The WooCommerce REST API uses a consumer key and secret as HTTP basic auth over HTTPS. Most integration problems are one of three things: a 401 from the wrong auth method, a server stripping the header, or pagination nobody handled.
WooCommerce exposes orders, products, customers and coupons over a REST API that lives alongside the WordPress one at /wp-json/wc/v3/. It is well documented and straightforward once running.
Getting it running trips people on a small number of specific things, which is what this covers — along with the failure that only appears in production, when a shop has more than ten of something.
Table of contents
- Generating keys
- Authenticating
- Pagination, which breaks silently
- Filtering rather than fetching everything
- Webhooks instead of polling
- Where the integration runs
- How this fits the rest of the stack
- FAQ
Generating keys
WooCommerce, Settings, Advanced, REST API, Add key. Pick a descriptive name, choose the user whose permissions the key inherits, and set Read or Read/Write.
The consumer secret is shown once. Copy it immediately — there is no way to retrieve it later, only to revoke the key and generate another.
Two things worth getting right at this point. The key inherits the permissions of the user you assign it to, so a key attached to an administrator can do everything that administrator can. Create a dedicated user with the narrowest role that works, and attach the key to that.
And generate a separate key per integration. When one is compromised or an integration is retired, you revoke one key rather than trying to work out what else was using the shared one.
Authenticating
Over HTTPS, the consumer key is the basic auth username and the secret is the password:
curl -u ck_xxxxxxxx:cs_xxxxxxxx \
https://shop.example.com/wp-json/wc/v3/orders
const auth = Buffer.from(`${key}:${secret}`).toString('base64');
const res = await fetch('https://shop.example.com/wp-json/wc/v3/orders?per_page=100', {
headers: { Authorization: `Basic ${auth}` },
});
HTTPS is not optional. Over plain HTTP the credentials are sent in a trivially decoded header, and WooCommerce falls back to a considerably more awkward OAuth 1.0a signing scheme instead. If you find yourself implementing OAuth signatures, the actual problem is that your site is not on HTTPS.
For 401 Unauthorized with correct credentials, the usual cause is a server that strips the Authorization header before PHP sees it — common on some Apache and CGI configurations. Test by passing credentials as query parameters:
curl "https://shop.example.com/wp-json/wc/v3/orders?consumer_key=ck_xxx&consumer_secret=cs_xxx"
If that works and the header does not, the header is being stripped. Fix it in the server config — on Apache, a SetEnvIf Authorization rule in .htaccess passes it through. Do not leave credentials in query strings as the permanent solution: they end up in access logs, proxy logs and browser history.
Pagination, which breaks silently
This is the bug that ships. The API returns 10 items by default, and code that does not paginate quietly processes the first 10 of everything — looking completely correct in testing against a shop with 8 orders.
The response headers carry the totals:
X-WP-Total— total items matching the query.X-WP-TotalPages— total pages at the currentper_page.
async function fetchAll(endpoint, params = {}) {
const out = [];
let page = 1;
let totalPages = 1;
do {
const qs = new URLSearchParams({ ...params, per_page: 100, page });
const res = await fetch(`${base}/${endpoint}?${qs}`, { headers });
if (!res.ok) throw new Error(`${endpoint} page ${page}: ${res.status}`);
totalPages = Number(res.headers.get('X-WP-TotalPages')) || 1;
out.push(...await res.json());
page += 1;
} while (page <= totalPages);
return out;
}
per_page maxes out at 100. Requesting more is silently capped rather than rejected, which is another way this fails quietly.
For large shops, offset pagination degrades badly — the database does more work for each successive page, and items shift between pages if orders are being created while you read. Filter by date instead and process in windows:
/wp-json/wc/v3/orders?after=2026-08-01T00:00:00&before=2026-08-08T00:00:00&per_page=100
Filtering rather than fetching everything
The most common performance mistake is pulling all orders and filtering client-side. The API filters server-side and it is dramatically cheaper:
# Orders by status and date
/wc/v3/orders?status=processing&after=2026-08-01T00:00:00
# Orders for one customer
/wc/v3/orders?customer=42
# Products by SKU or stock status
/wc/v3/products?sku=ABC-123
/wc/v3/products?stock_status=outofstock
# Only the fields you need -- much smaller responses
/wc/v3/orders?_fields=id,status,total,date_created
_fields is underused and worth adopting. A full order object is large — line items, addresses, metadata, tax lines — and if you need four fields, requesting four fields cuts response size and parsing time substantially.
For bulk writes, use the batch endpoint rather than a request per item. It handles up to 100 operations in one call and is far kinder to the server:
POST /wc/v3/products/batch
{
"update": [
{ "id": 12, "regular_price": "19.99" },
{ "id": 13, "stock_quantity": 40 }
]
}
Batch responses report per-item success, so check each result rather than assuming a 200 on the request means all 100 operations succeeded.
Webhooks instead of polling
If your integration polls for new orders every minute, most of those requests find nothing and every one of them costs a full WordPress bootstrap. Webhooks push instead.
WooCommerce, Settings, Advanced, Webhooks. Choose a topic — order.created, order.updated, product.updated — and a delivery URL.
Three things a webhook receiver must do:
- Verify the signature. The
X-WC-Webhook-Signatureheader is an HMAC-SHA256 of the raw body using the webhook secret. Compute it over the raw body before any JSON parsing, or it will never match. - Respond quickly, then process asynchronously. WooCommerce times out slow endpoints and marks the webhook as failed; enough failures and it disables the webhook entirely.
- Be idempotent. Deliveries can repeat. Key on the order ID and make reprocessing harmless.
Webhooks with polling as a periodic reconciliation pass is the robust combination: the webhook handles the normal case with low latency, and a daily sweep catches anything that failed delivery while your endpoint was down.
Where the integration runs
An integration is a small always-on service: it holds credentials, receives webhooks at a stable HTTPS URL, retries failures, and keeps enough state to be idempotent. A script on someone’s laptop is not that.
Concretely it needs a public URL with a valid certificate, secrets kept outside the code, a database for processed-event IDs and sync state, and logs you can read when a merchant says an order did not come through.
On RunxBuild, that service deploys from a repository — Node, Python, Go, PHP, whatever you wrote it in — with a build log, a live route, custom domains, environment variables for the API keys, runtime logs and rollback to the previous deploy, with a managed MySQL or Postgres alongside for the sync state. If the shop itself is the thing you are hosting, managed WordPress starts at $3/month with a file manager and database browser in the dashboard.
How this fits the rest of the stack
Generate a scoped key pair, authenticate with basic auth over HTTPS, and read X-WP-TotalPages — the pagination bug is the one that passes testing and fails on a real shop. Filter server-side, use _fields and the batch endpoint, and prefer signed webhooks over polling with a reconciliation sweep behind them. The integration is a small always-on service with a database, and the RunxBuild hosting calculator shows that service and its database as line items.
Useful related references:
- WooCommerce Alternatives: What You Are Actually Trying to Escape
- Taking Payments on WordPress Without WooCommerce
- WooCommerce Shortcodes: The Full List and Why Blocks Replaced Most of Them
- Services on RunxBuild
FAQ
How do I authenticate with the WooCommerce REST API?
Over HTTPS, use the consumer key as the basic auth username and the consumer secret as the password. Generate the pair in WooCommerce, Settings, Advanced, REST API. The secret is shown only once, so copy it immediately.
Why do I get 401 Unauthorized with correct WooCommerce API keys?
Usually the server is stripping the Authorization header before PHP sees it, which is common on some Apache and CGI setups. Test by passing consumer_key and consumer_secret as query parameters — if that works, fix the header pass-through in the server config rather than leaving credentials in URLs.
Why does the WooCommerce API only return 10 items?
That is the default per_page. Set it up to a maximum of 100 and paginate using the X-WP-TotalPages response header. Requesting more than 100 is silently capped, which is why this bug often passes testing on a small shop.
How do I fetch only specific fields from the WooCommerce API?
Use the _fields query parameter, for example ?_fields=id,status,total. Full order objects include line items, addresses, tax lines and metadata, so restricting fields substantially reduces response size and parsing time.
Should I use webhooks or polling with WooCommerce?
Webhooks for the normal path, with a periodic reconciliation poll as a safety net. Verify the X-WC-Webhook-Signature HMAC against the raw request body, respond fast and process asynchronously, and make handling idempotent since deliveries can repeat.