Use the WooCommerce REST API for Headless and Decoupled Commerce

The WooCommerce REST API exposes the full WooCommerce data model — products, orders, customers, coupons, reports, and settings — as authenticated JSON endpoints, enabling headless commerce architectures where a Next.js, Nuxt, or Astro front-end renders the storefront while WordPress and WooCommerce handle inventory, checkout processing, payment gateway integration, and order management in the background. The API is built on WordPress’s WP REST API infrastructure, extending it with WooCommerce-specific namespaces: /wp-json/wc/v3/products, /wp-json/wc/v3/orders, /wp-json/wc/v3/customers. Authentication uses WooCommerce-specific Consumer Key / Consumer Secret pairs (generated in WooCommerce → Settings → Advanced → REST API) with OAuth 1.0a for HTTP connections and HTTP Basic Auth over HTTPS — the keys are not WordPress Application Passwords and are stored separately in the woocommerce_api_keys table. The product endpoint supports full CRUD: GET /wc/v3/products returns a paginated list with filter parameters (?status=publish&category=15&per_page=20&page=2), POST /wc/v3/products creates a product, PUT /wc/v3/products/{id} updates it. Variations, attributes, and images are nested within the product object rather than separate endpoints, making a single API call sufficient to fetch all data needed to render a product detail page. The Orders endpoint supports creating orders programmatically (for custom checkout flows that bypass the WooCommerce cart), updating order status, and querying orders by customer, date range, or status. Rate limiting and caching are the primary technical challenges for high-traffic headless stores — the WooCommerce API does not include built-in rate limiting, so this must be added at the server level (Nginx limit_req or an API gateway). The order lifecycle post covers server-side order event hooks; this post covers the REST API layer for external clients and headless frontends.

Problem: A WooCommerce store is being migrated to a Next.js headless frontend — the new site needs to fetch products with their variants, update cart quantities, submit orders, and retrieve order status, all from JavaScript running outside the WordPress environment without loading any WordPress PHP on the front-end server.

Solution: Use the WooCommerce REST API v3 with Consumer Key/Secret authentication — fetch products, create carts via the Store API (the Blocks-based public cart/checkout API), and query order status from a Node.js API layer that proxies requests to the WooCommerce backend.

// woocommerce-client.js — server-side Node.js module (never expose keys to browser)

const WC_URL         = process.env.WC_URL;             // e.g. https://store.example.com
const CONSUMER_KEY   = process.env.WC_CONSUMER_KEY;
const CONSUMER_SECRET= process.env.WC_CONSUMER_SECRET;

// Base64 Basic Auth header (HTTPS required)
const AUTH_HEADER = 'Basic ' + Buffer.from(`${CONSUMER_KEY}:${CONSUMER_SECRET}`).toString('base64');

async function wcFetch(path, options = {}) {
    const url = `${WC_URL}/wp-json/wc/v3${path}`;
    const res = await fetch(url, {
        ...options,
        headers: {
            'Authorization': AUTH_HEADER,
            'Content-Type':  'application/json',
            ...options.headers,
        },
    });
    if (!res.ok) {
        const error = await res.json().catch(() => ({}));
        throw new Error(`WC API ${res.status}: ${error.message || res.statusText}`);
    }
    return res.json();
}

// ── Fetch products with filtering and pagination ──────────────────────────
export async function getProducts({ page = 1, perPage = 12, categoryId, search } = {}) {
    const params = new URLSearchParams({
        status:   'publish',
        per_page: perPage,
        page,
        ...(categoryId && { category: categoryId }),
        ...(search     && { search }),
    });
    return wcFetch(`/products?${params}`);
}

// ── Fetch single product with all variations ──────────────────────────────
export async function getProduct(id) {
    const [product, variations] = await Promise.all([
        wcFetch(`/products/${id}`),
        wcFetch(`/products/${id}/variations?per_page=100`),
    ]);
    return { ...product, variations };
}

// ── Create an order programmatically (custom checkout) ───────────────────
export async function createOrder(orderData) {
    return wcFetch('/orders', {
        method: 'POST',
        body: JSON.stringify({
            payment_method:       orderData.paymentMethod,
            payment_method_title: orderData.paymentMethodTitle,
            set_paid:             false,
            billing:              orderData.billing,
            shipping:             orderData.shipping,
            line_items:           orderData.items.map(item => ({
                product_id:   item.productId,
                variation_id: item.variationId,
                quantity:     item.quantity,
            })),
            shipping_lines: [{
                method_id:    orderData.shippingMethodId,
                method_title: orderData.shippingMethodTitle,
                total:        String(orderData.shippingTotal),
            }],
            coupon_lines: orderData.coupons?.map(code => ({ code })) ?? [],
        }),
    });
}

// ── Check order status (for order tracking page) ──────────────────────────
export async function getOrderStatus(orderId, orderKey) {
    // orderKey is the wc-order-key query param from the confirmation email
    const order = await wcFetch(`/orders/${orderId}`);
    if (order.order_key !== orderKey) {
        throw new Error('Order key mismatch');
    }
    return { status: order.status, total: order.total, items: order.line_items };
}

NOTE: For the cart and checkout steps in a headless WooCommerce store, use the Store API (/wp-json/wc/store/v1/cart) rather than the authenticated WC REST API — the Store API is designed for unauthenticated frontend requests, uses nonce-based CSRF protection via the Nonce header, and handles guest sessions via cookies. The authenticated WC v3 REST API is for server-to-server admin operations (inventory management, order processing, reporting). Mixing the two — using the admin API from the browser — exposes your Consumer Keys in JavaScript source code, which is a critical security vulnerability regardless of HTTPS encryption.