Skip to content
ExponaBLOG
WORDPRESS DEVELOPMENT & PLUGINS · DAY 051 / 100

WooCommerce REST API: WordPress best practices explained

Integrate stores with other systems.

Expona Admin7 min read

WooCommerce REST API: WordPress best practices explained

Connecting your WooCommerce store to external systems—inventory management platforms, accounting software, custom dashboards, or mobile apps—requires a robust integration layer. The WooCommerce REST API is that layer, but like any API, it demands careful attention to authentication, permissions, rate limits, and data consistency. We'll walk through the production patterns we've used across e-commerce platforms to integrate stores safely and reliably.

Keys — Generate consumer keys with scoped permissions

Authentication in WooCommerce REST API flows through consumer keys and secrets. Rather than using your main WordPress credentials, you generate application-specific key pairs, each with granular permission scopes. This is non-negotiable in production: if an external service is compromised, you revoke that key without touching your entire system.

Navigate to WooCommerce → Settings → Advanced → REST API Keys, or use the REST API menu in WordPress admin. Click "Create an API key" and assign a description (e.g., "Inventory sync service" or "Mobile app backend"). Choose a user—typically a dedicated service account—and set permissions:

  • Read — Query products, orders, customers without modification.
  • Write — Create or update resources.
  • Delete — Remove resources (rarely needed).

A real scenario: your third-party inventory system needs to read products and update stock levels, but never delete orders or access customer data. You'd grant it Read/Write on products only, not orders or customers. This containment saves you if that system's API key leaks.

The generated key and secret look like this:

php
// Example (not real)
Key: ck_live_a1b2c3d4e5f6g7h8i9j0
Secret: cs_live_z9y8x7w6v5u4t3s2r1q0p

Store these in environment variables or a secrets manager, never in code. When authenticating REST calls, use HTTP Basic Auth with the key as username and secret as password. WordPress REST API clients (like axios or fetch) handle this transparently:

php
const response = await fetch('https://yourstore.com/wp-json/wc/v3/products', {
  method: 'GET',
  headers: {
    'Authorization': 'Basic ' + Buffer.from('ck_live_xxx:cs_live_xxx').toString('base64'),
    'Content-Type': 'application/json'
  }
});

Rotate keys quarterly and audit which external systems hold active credentials. If a service is decommissioned, delete its key immediately.

Orders and products — CRUD over HTTP

The REST API exposes core WooCommerce resources via standard HTTP verbs. For most integrations, you'll work with orders and products.

Reading products

Fetch product listings with filtering, sorting, and pagination. A sync service might poll for updated products hourly:

php
GET /wp-json/wc/v3/products?per_page=100&page=1&order=desc&orderby=date_modified

This returns products updated most recently first, 100 per page. Response includes SKU, price, stock level, description, and images. For bulk syncs, iterate pages until you hit the final page (check response headers for pagination metadata).

Updating products

Change stock, price, or description for a single product:

php
PUT /wp-json/wc/v3/products/42
{
  "stock_quantity": 150,
  "regular_price": "29.99"
}

WooCommerce validates the payload—invalid price formats or negative stock are rejected with clear 400 errors. Always check the response status.

Order CRUD

Read orders with filters (date range, status, customer):

php
GET /wp-json/wc/v3/orders?status=processing&after=2025-01-01T00:00:00Z&per_page=50

Retrieve order details: items, customer info, payment status, shipping address. External accounting software might fetch orders daily, then POST them to a ledger system.

Creating orders programmatically (less common but valid):

php
POST /wp-json/wc/v3/orders
{
  "payment_method": "bacs",
  "payment_method_title": "Direct Bank Transfer",
  "set_paid": false,
  "billing": {
    "first_name": "John",
    "last_name": "Doe",
    "email": "john@example.com",
    "phone": "1234567890",
    "address_1": "123 Main St",
    "city": "Springfield",
    "state": "IL",
    "postcode": "62701",
    "country": "US"
  },
  "line_items": [
    {
      "product_id": 42,
      "quantity": 2
    }
  ]
}

This is useful for B2B integrations where orders originate in a partner system and you want WooCommerce as the order source of truth.

Webhooks — Notify external systems on order events

Instead of polling the API every hour, webhooks let WooCommerce push events to external services. When an order is placed or status changes, WooCommerce sends an HTTP POST to your endpoint.

Create webhooks in WordPress admin: WooCommerce → Settings → Advanced → Webhooks. Specify:

  • Event — order.created, order.updated, order.deleted, product.created, etc.
  • Topic — The resource type and action.
  • Delivery URL — Your external service's endpoint.

A webhook payload for order.created looks like:

php
{
  "id": 1001,
  "parent_id": 0,
  "number": "1001",
  "order_key": "wc_order_x7y8z9a0b1c2",
  "created_via": "checkout",
  "version": "8.4.0",
  "status": "processing",
  "currency": "USD",
  "date_created": "2025-02-15T10:30:00",
  "date_modified": "2025-02-15T10:32:00",
  "discount_total": "0.00",
  "discount_tax": "0.00",
  "shipping_total": "10.00",
  "shipping_tax": "0.00",
  "cart_tax": "0.00",
  "total": "99.99",
  "total_tax": "0.00",
  "customer_id": 15,
  "order_key": "...",
  "line_items": [ { "id": 1, "name": "Product Name", "product_id": 42, "quantity": 1, "subtotal": "89.99" } ],
  "billing": { "first_name": "John", ... },
  "shipping": { ... }
}

Your service receives this, processes it, and responds with HTTP 200 within 10 seconds. WooCommerce retries failed deliveries up to 5 times over several hours, so your endpoint should be idempotent (process the same order twice without duplicating records).

In your fulfillment system or CRM, consume the webhook:

php
// Node.js + Express example
app.post('/webhooks/woocommerce/order', (req, res) => {
  const order = req.body;
  
  // Idempotency: check if order already processed
  if (db.orderExists(order.id)) {
    console.log(`Order ${order.id} already processed`);
    return res.status(200).json({ status: 'already_processed' });
  }
  
  // Create shipment, update ledger, etc.
  fulfillmentService.createShipment(order);
  
  res.status(200).json({ status: 'success', order_id: order.id });
});

Webhooks are far more efficient than polling, especially for high-volume stores. They reduce latency and server load dramatically.

Rate limits — Batch endpoints for bulk updates

WooCommerce REST API rate-limits authenticated requests: typically 10 requests per second per API key. A sync service updating 10,000 products one-by-one would hit this limit immediately and slow to a crawl.

The batch endpoint is your solution. Send multiple operations in a single request:

php
POST /wp-json/wc/v3/products/batch
{
  "update": [
    { "id": 1, "stock_quantity": 100 },
    { "id": 2, "stock_quantity": 150 },
    { "id": 3, "stock_quantity": 200 }
  ]
}

This counts as one request toward your rate limit, not three. The response contains success/failure status for each item:

php
{
  "update": [
    { "id": 1, "stock_quantity": 100, "date_modified": "2025-02-15T10:35:00" },
    { "id": 2, "stock_quantity": 150, "date_modified": "2025-02-15T10:35:00" },
    { "id": 3, "stock_quantity": 200, "date_modified": "2025-02-15T10:35:00" }
  ]
}

Batch endpoints work for products, orders, and customers. Always batch when updating more than a handful of records. Split large jobs into chunks of 100 items per request.

Monitor your rate-limit headers:

php
// Response headers
X-WC-Total: 5000       // Total items available
X-WC-TotalPages: 50    // Pages of results
X-WC-PageSize: 100     // Items per page

If you're hitting limits, implement exponential backoff and stagger requests across multiple service instances.

Common mistakes

  • Storing secrets in code. API keys in git repositories are harvested by automated scaners within hours. Use environment variables, .env files (excluded from git), or a secrets manager.
  • Polling instead of webhooks. Scheduling a sync job every 5 minutes generates unnecessary API calls and delays event handling. Use webhooks for real-time or near-real-time integrations.
  • Ignoring pagination. Assuming all results fit in one response. Always handle the X-WC-TotalPages header and iterate through pages.
  • No idempotency. Processing the same webhook twice creates duplicate orders or shipments. Include a unique idempotency key or check for existing records before writing.
  • Overly broad permissions. Granting "Read/Write" on all resources when you only need product stock updates. Narrow scopes reduce blast radius if credentials leak.
  • Not handling errors gracefully. Assuming every API call succeeds. WooCommerce returns detailed error objects (400, 404, 500). Log them, retry with backoff, and alert on persistent failures.

Key takeaways

  • Generate application-specific API keys with minimal scoped permissions—never share WordPress admin credentials with third-party systems.
  • Use REST endpoints for CRUD operations on products, orders, and customers; familiarize yourself with filtering, sorting, and pagination parameters.
  • Prefer webhooks over polling for event-driven integrations; they're faster, cheaper, and more reliable at scale.
  • Batch multiple updates into single requests to respect rate limits and improve throughput for bulk operations.
  • Implement idempotency and error handling in all integration code; assume network failures and duplicate events.
  • Rotate API keys quarterly and audit active integrations regularly.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles