Skip to content

WooCommerce REST API Adapter

v3 Compatible

Last updated: May 15, 2026

A drop-in compatible REST API for STOAR that mirrors WooCommerce v3's wire format — same URL paths (/wp-json/wc/v3/...), same flat query-parameter syntax, same JSON response field names, same auth options. Existing WooCommerce client libraries (e.g. automattic/woocommerce PHP/JS, klarna/woocommerce-rest-api Node) can talk to STOAR without modification.

The adapter is the second implementation of STOAR's extensible API adapter framework (the first being Magento). It demonstrates how a fundamentally different vendor flavour — flat query params instead of nested searchCriteria[…], Basic Auth instead of Bearer-only, status vocabulary processing/completed instead of paid/delivered — sits next to Magento on the same data layer.


Table of contents #


Quick start #

# 1. Get a Sanctum token with woocommerce:admin ability — issued via /manager/api-tokens
#    or via the Magento token endpoint (POST /rest/V1/integration/admin/token), then
#    grant the woocommerce:admin ability inside /manager.
TOKEN="2|L5pXj63e881lkgtHPK7Hjozyq7Pb3trHCy0iaDRf76405f72"

# 2. List the last 5 orders (Bearer auth — STOAR extension; works alongside Basic)
curl -s "https://app.stoar.ai/wp-json/wc/v3/orders?per_page=5&orderby=date&order=desc" \
  -H "Authorization: Bearer $TOKEN" \
  | jq '.[] | {id, status, total, currency}'

# 3. Use HTTP Basic just like a real WooCommerce store
curl -s "https://app.stoar.ai/wp-json/wc/v3/products?per_page=3" \
  -u "any-key:$TOKEN"

# 4. Or stuff credentials into the query string (still over HTTPS only)
curl -s "https://app.stoar.ai/wp-json/wc/v3/products?consumer_key=any&consumer_secret=$TOKEN"

Authentication #

WooCommerce supports three auth flavours; the adapter accepts all of them and converts them into the same Sanctum personal-access-token check internally. Tokens are issued from STOAR's /manager/api-tokens page (or programmatically via AdminUser::createToken('label', ['woocommerce:admin'])).

Method Format Use case
HTTP Basic Authorization: Basic base64(consumer_key:consumer_secret) Default for most WC client libraries — over HTTPS only
Query params ?consumer_key=…&consumer_secret=… Quick curl testing — over HTTPS only
Bearer (STOAR extension) Authorization: Bearer <token> Native Sanctum — interchangeable with the Magento adapter

Mapping to Sanctum

The adapter ignores consumer_key (you can pass any string — "any-key" works). The consumer_secret MUST be a valid Sanctum personal-access token whose abilities array contains woocommerce:admin.

This mapping is purely an interpretation layer; from STOAR's perspective, every WC request is just a Bearer-authenticated Sanctum request. The same personal-access-token row backs both:

  • a Magento client using /rest/V1/orders (needs magento:admin ability)
  • a WooCommerce client using /wp-json/wc/v3/orders (needs woocommerce:admin ability)

You can issue a single token with both abilities, or separate ones — your call.

Customer tokens

Customer-scoped endpoints are not yet available. The woocommerce:customer ability is reserved for future use.


Endpoints #

All paths are relative to /wp-json/wc/v3 (verbatim WordPress REST namespace).

GET /wp-json/wc/v3/orders

List orders.

Authorization: Bearer / Basic / query, with woocommerce:admin ability.

Query params: see Query parameters.

Response (200): flat JSON array of orders. Pagination travels in headers, NOT inside the body:

HTTP/1.1 200 OK
X-WP-Total:      150
X-WP-TotalPages: 8
Content-Type:    application/json

[
  { /* order */ },
  { /* order */ }
]

Why headers, not envelope? This is WooCommerce's actual convention — different from Magento's {items, search_criteria, total_count} wrapper. WC client libraries (Automattic's official PHP/JS clients) read X-WP-Total to drive their pagination cursors.


GET /wp-json/wc/v3/orders/{id}

Single order by numeric id.

Response (200): see Response shape.

Errors:

{
  "code":    "woocommerce_rest_shop_order_invalid_id",
  "message": "Invalid shop_order ID.",
  "data":    { "status": 404, "id": 99999 }
}

GET /wp-json/wc/v3/orders/{id}/notes

Lists the order's status-history rows reshaped as WooCommerce notes.

Response (200):

[
  {
    "id":               42,
    "author":           "system",
    "date_created":     "2026-04-10T12:00:00+00:00",
    "date_created_gmt": "2026-04-10T12:00:00+00:00",
    "note":             "Payment received",
    "customer_note":    false,
    "_links":           { "self": [...], "collection": [...], "up": [...] }
  }
]

customer_note is always false because Stoar does not distinguish customer-visible notes from admin-only ones at the data level.


POST /wp-json/wc/v3/orders/{id}/notes

Adds a note (mapped onto a new OrderStatusLog row in Stoar).

Request:

{ "note": "Customer phoned to confirm delivery slot" }

customer_note (bool) is accepted but currently has no effect.

Response (201): the new note in the same shape as GET .../notes items.

Errors:

  • 400 rest_invalid_param — missing note
  • 404 — unknown order id

GET /wp-json/wc/v3/orders/{id}/refunds

Lists refunds for an order. Stoar has no separate refund table — the adapter returns either:

  • [] when refunded_amount === 0, or
  • [{...}] with one synthetic refund row whose id === parent_id === order.id
[
  {
    "id":               123,
    "parent_id":        123,
    "date_created":     "2026-04-12T08:00:00+00:00",
    "amount":           "30.00",
    "reason":           "",
    "refunded_by":      0,
    "refunded_payment": true,
    "meta_data":        [],
    "line_items":       [],
    "api_refund":       true
  }
]

POST /wp-json/wc/v3/orders/{id}/refunds

Issues a refund. Delegates to Stoar's existing Order::processRefund() which calls Stripe via StripeService::processRefund(). Stoar marks the order refunded on a full refund, or accumulates refunded_amount for partials.

Request:

{ "amount": 30, "reason": "Customer changed mind" }

Both fields are optional. If amount is omitted, Stoar refunds the full remaining refundable balance.

Response (201): the refund row in the shape above.

Errors:

  • 404 — unknown order id
  • 422 woocommerce_rest_invalid_state — order is not refundable (no Stripe payment intent, status not paid/processing/shipped/delivered)

GET /wp-json/wc/v3/products

List products.

Query params: see Query parameters.

Response (200): flat array, with X-WP-Total / X-WP-TotalPages headers.


GET /wp-json/wc/v3/products/{id}

Single product by numeric id (NOT SKU like Magento).

Response (200): see Response shape.

Errors: 404 woocommerce_rest_product_invalid_id.


Query parameters #

WooCommerce uses flat query-string parameters — far simpler than Magento's nested searchCriteria[…] grammar. Translation into the same vendor-agnostic OrderQuery / ProductQuery value objects happens inside the adapter's Search/OrderQueryParser and Search/ProductQueryParser classes.

Common params (orders + products)

Param Default Purpose
per_page 10 Results per page (max 100)
page 1 Page number, 1-indexed
orderby date Sort field — see per-resource lists below
order desc asc or desc
include Comma-separated id list (?include=1,2,3)
exclude Comma-separated id list to exclude
search Substring match — see per-resource notes

Orders only

Param Example Effect
status processing,completed CSV — translated to Stoar status values; OR'd within the group
customer 42 Filter by customer_id
after 2024-01-01T00:00:00 created_at >= …
before 2024-12-31T23:59:59 created_at <= …
search jane LIKE on customer_email
orderby values date (default), id, include, title datecreated_at, titlecustomer_email

Products only

Param Example Effect
status publish, draft Translated to Stoar active / inactive
type simple, variable variable translates to Stoar configurable
featured true / false Filters on is_featured
category 5 Single category id
sku WID-1 Exact match
slug widget-pro Exact match
min_price 10 price >= …
max_price 100 price <= …
search widget LIKE on product name
orderby values date, id, include, title, price, slug titlename, slugslug

Unknown parameters are silently ignored (matching WC behaviour). Filter values that don't translate to anything sensible (e.g. ?status=on-hold for products) result in an empty group, not a 400.


Field mapping (WooCommerce ↔ STOAR) #

Order

WooCommerce field STOAR source Notes
id id numeric
number id (cast to string) WC convention
order_key lookup_token Stoar's per-order magic-link token
status status (translated) see Status translation
currency currency (uppercased) eurEUR
total total_amount 2-dp string
cart_tax, total_tax tax_amount 2-dp string
shipping_total shipping_amount 2-dp string
discount_total discount_amount 2-dp string
customer_id customer_id ?? 0 guests get 0
customer_note customer_notes
billing.* billing_info JSON column flat WC shape
shipping.* shipping_info JSON column flat WC shape
payment_method first non-archived OrderPayment.gateway falls back to Order.payment_method
payment_method_title derived from gateway key stripe → "Credit / Debit Card", etc.
transaction_id stripe_payment_intent_id
date_created, date_modified created_at, updated_at ISO8601
date_paid first succeeded OrderPayment.created_at null if no payment recorded
date_completed updated_at if status === delivered null otherwise
line_items[] Order.items (with variant + product) see below
coupon_lines[] derived from coupon_code + discount_amount empty when no coupon
refunds[] one synthetic entry when refunded_amount > 0 see Refunds endpoints
meta_data[] always includes _stoar_status and _stoar_lookup_token extension data

Order line_item

WC field Stoar source
id OrderItem.id
name OrderItem.name
product_id OrderItem.product_id ?? 0
variation_id OrderItem.variant_id ?? 0
quantity OrderItem.quantity
subtotal, total price * quantity (2-dp string)
subtotal_tax, total_tax OrderItem.tax_amount
sku variant SKU first, falls back to product SKU
price OrderItem.price

Product

WC field STOAR source Notes
id id
name, slug, sku, description, short_description passthrough
permalink url('/product/' . slug)
type type (translated) configurablevariable
status status (translated) activepublish
featured is_featured bool
regular_price price (raw) 2-dp string
sale_price special_price (or "" when null) 2-dp string
price min(regular, sale) 2-dp string
on_sale special_price !== null && special_price < price
purchasable stock > 0 && status === 'active'
manage_stock always true
stock_quantity stock; for configurable, sum of variant stock
stock_status instock / outofstock
weight weight (string)
tax_class tax_class_id (string)
categories[] one element from category relation Stoar has a single primary category
images[] image_path + gallery_paths array first entry is primary
variations[] variant ids (only for variable products)
meta_data[] always includes _stoar_id, _stoar_low_stock_threshold, _stoar_image_prompt
attributes[], default_attributes[] [] Stoar has no global attribute system
tags[], related_ids[], upsell_ids[], cross_sell_ids[] [] not modelled in Stoar
dimensions empty not tracked
average_rating, rating_count "0.00", 0 not aggregated at this level

Status translation #

WooCommerce and Stoar use different status vocabularies. The adapter translates bidirectionally: when filtering (?status=processing), WC values are mapped to Stoar; when serialising responses, Stoar's status is mapped back to WC.

Order status

WooCommerce Stoar Notes
pending pending unpaid, awaiting payment
processing paid (rendered) / accepts paid (filter) "payment captured, fulfilment in progress"
processing processing, shipped (rendered) Stoar's processing and shipped both render as WC processing
completed delivered
cancelled cancelled
refunded refunded
on-hold pending (filter only) Stoar has no explicit on-hold state
failed cancelled (filter only) best-fit; Stoar has no explicit failed state

Product status

WooCommerce Stoar
publish active
draft, pending, private inactive

Product type

WooCommerce Stoar
simple simple
variable configurable
grouped, external (unsupported — not in Stoar)

Filter values that map to null (e.g. ?status=trash for products) are silently ignored — they don't error.


Response shape #

Order — annotated example

{
  "id":             456,
  "parent_id":      0,
  "number":         "456",
  "order_key":      "abc123…",
  "created_via":    "checkout",
  "version":        "8.5.0",
  "status":         "processing",
  "currency":       "EUR",
  "date_created":   "2026-04-10T09:00:00+00:00",
  "date_modified":  "2026-04-10T09:30:00+00:00",
  "discount_total": "0.00",
  "discount_tax":   "0.00",
  "shipping_total": "5.00",
  "shipping_tax":   "0.00",
  "cart_tax":       "10.00",
  "total":          "115.00",
  "total_tax":      "10.00",
  "prices_include_tax": false,
  "customer_id":    45,
  "customer_note":  "",
  "billing":        { /* WC address shape (flat) */ },
  "shipping":       { /* WC address shape (flat) */ },
  "payment_method": "stripe",
  "payment_method_title": "Credit / Debit Card",
  "transaction_id": "pi_test_…",
  "date_paid":      "2026-04-10T09:05:00+00:00",
  "date_completed": null,
  "cart_hash":      "",
  "meta_data":      [
    { "id": 0, "key": "_stoar_status",       "value": "paid" },
    { "id": 0, "key": "_stoar_lookup_token", "value": "abc123…" }
  ],
  "line_items": [
    {
      "id":            456,
      "name":          "Widget",
      "product_id":    789,
      "variation_id":  12,
      "quantity":      2,
      "tax_class":     "",
      "subtotal":      "100.00",
      "subtotal_tax":  "10.00",
      "total":         "100.00",
      "total_tax":     "10.00",
      "taxes":         [],
      "meta_data":     [],
      "sku":           "WID-1-A",
      "price":         50
    }
  ],
  "tax_lines":      [],
  "shipping_lines": [
    {
      "id":           0,
      "method_title": "Standard",
      "method_id":    "flat_rate",
      "instance_id":  "",
      "total":        "5.00",
      "total_tax":    "0.00",
      "taxes":        [],
      "meta_data":    []
    }
  ],
  "fee_lines":    [],
  "coupon_lines": [],
  "refunds":      [],
  "_links":       {
    "self":       [{ "href": "https://app.stoar.ai/wp-json/wc/v3/orders/456" }],
    "collection": [{ "href": "https://app.stoar.ai/wp-json/wc/v3/orders" }]
  }
}

Product — abbreviated example

{
  "id":                 789,
  "name":               "Widget",
  "slug":               "widget",
  "permalink":          "https://app.stoar.ai/product/widget",
  "type":               "simple",
  "status":             "publish",
  "featured":           true,
  "catalog_visibility": "visible",
  "description":        "<p>A great widget</p>",
  "short_description":  "Great widget",
  "sku":                "WID-1",
  "price":              "79.99",
  "regular_price":      "99.99",
  "sale_price":         "79.99",
  "on_sale":            true,
  "purchasable":        true,
  "manage_stock":       true,
  "stock_quantity":     42,
  "stock_status":       "instock",
  "weight":             "1.5",
  "categories":         [{ "id": 5, "name": "Widgets", "slug": "widgets" }],
  "images":             [
    { "id": 0, "src": "products/widget.webp", "name": "primary",   "position": 0, "alt": "" },
    { "id": 0, "src": "products/g1.webp",     "name": "gallery-1", "position": 1, "alt": "" }
  ],
  "attributes":         [],
  "variations":         [],
  "meta_data":          [
    { "id": 0, "key": "_stoar_id", "value": "789" },
    { "id": 0, "key": "_stoar_low_stock_threshold", "value": "3" }
  ],
  "_links":             {
    "self":       [{ "href": "https://app.stoar.ai/wp-json/wc/v3/products/789" }],
    "collection": [{ "href": "https://app.stoar.ai/wp-json/wc/v3/products" }]
  }
}

For a configurable / variable product, variations is populated with variant ids, and stock_quantity is the sum of variant stock.


Real-world example #

Live response from production for GET /wp-json/wc/v3/orders/10126 (USD $936.98, two simple-product line items, paid via PayID). PII anonymised.

Request

GET /wp-json/wc/v3/orders/10126
Authorization: Bearer 2|L5pXj63e881lkgtHPK7Hjozyq7Pb3trHCy0iaDRf76405f72

Response

{
  "id":               10126,
  "parent_id":        0,
  "number":           "10126",
  "order_key":        "REDACTED-FOR-DOCS",
  "created_via":      "checkout",
  "version":          "8.5.0",
  "status":           "processing",
  "currency":         "USD",
  "date_created":     "2025-06-03T04:56:43+00:00",
  "date_modified":    "2025-06-03T04:56:43+00:00",
  "discount_total":   "0.00",
  "discount_tax":     "0.00",
  "shipping_total":   "0.00",
  "shipping_tax":     "0.00",
  "cart_tax":         "0.00",
  "total":            "936.98",
  "total_tax":        "0.00",
  "prices_include_tax": false,
  "customer_id":      5794,
  "customer_note":    "",
  "billing": {
    "first_name": "Jane",
    "last_name":  "Doe",
    "company":    "",
    "address_1":  "1 Example Street",
    "address_2":  "",
    "city":       "Phoenix",
    "state":      "AZ",
    "postcode":   "85001",
    "country":    "US",
    "email":      "",
    "phone":      "+1-555-0100"
  },
  "shipping":         { /* same shape as billing */ },
  "payment_method":   "payid",
  "payment_method_title": "PayID",
  "transaction_id":   "",
  "date_paid":        null,
  "date_completed":   null,
  "cart_hash":        "",
  "meta_data": [
    { "id": 0, "key": "_stoar_status",       "value": "paid" },
    { "id": 0, "key": "_stoar_lookup_token", "value": "REDACTED-FOR-DOCS" }
  ],
  "line_items": [
    {
      "id":           30219,
      "name":         "Reloop Terminal Mix 8",
      "product_id":   112238,
      "variation_id": 95589,
      "quantity":     3,
      "tax_class":    "",
      "subtotal":     "897.00",
      "subtotal_tax": "0.00",
      "total":        "897.00",
      "total_tax":    "0.00",
      "taxes":        [],
      "meta_data":    [],
      "sku":          "RELOOP_TERMINALMIX8_025-DEF",
      "price":        299
    },
    {
      "id":           30220,
      "name":         "Premium Skateboard Socks",
      "product_id":   51706,
      "variation_id": 33857,
      "quantity":     2,
      "tax_class":    "",
      "subtotal":     "39.98",
      "subtotal_tax": "0.00",
      "total":        "39.98",
      "total_tax":    "0.00",
      "taxes":        [],
      "meta_data":    [],
      "sku":          "SK8-SOCK-027-DEF",
      "price":        19.99
    }
  ],
  "tax_lines":      [],
  "shipping_lines": [
    {
      "id":           0,
      "method_title": "Free Shipping",
      "method_id":    "flat_rate",
      "instance_id":  "",
      "total":        "0.00",
      "total_tax":    "0.00",
      "taxes":        [],
      "meta_data":    []
    }
  ],
  "fee_lines":    [],
  "coupon_lines": [],
  "refunds":      [],
  "_links":       {
    "self":       [{ "href": "https://app.stoar.ai/wp-json/wc/v3/orders/10126" }],
    "collection": [{ "href": "https://app.stoar.ai/wp-json/wc/v3/orders" }]
  }
}

Caveats integrators should know

These overlap with Magento's caveats — the underlying Stoar data is the same, just rendered in a different envelope:

Field Observed Why
transaction_id, date_paid "" / null for paid orders Pulled from Stoar's OrderPayment audit log; legacy orders predating the log show empty
payment_method "payid" This is the STOAR gateway-key (stripe / bank_transfer / cash_on_delivery / payid / invoice) — not a WC-canonical method name
payment_method_title derived from gateway key hardcoded mapping — patch in OrderResource::paymentMethodTitle() to extend
version always "8.5.0" hardcoded — not the WC version really running
created_via always "checkout" Stoar has no API-create flow yet
customer_ip_address, customer_user_agent always "" not stored on Order
cart_hash always "" not stored
tax_lines always [] Stoar tracks tax at the order level only, not per jurisdiction
attributes, default_attributes (products) always [] Stoar has no global attribute taxonomy
meta_data._stoar_lookup_token per-order magic-link token Same security warning as Magento — possession is sufficient to view the order without auth. Never expose this to client-side code or logs.

Error shape #

WooCommerce errors use:

{
  "code":    "machine_readable_code",
  "message": "Human-readable message",
  "data":    { "status": 404, "id": 99999 }
}

…NOT Magento's {message, parameters, trace}.

HTTP Trigger code
400 bad query param / missing required body rest_invalid_param
401 missing or invalid token woocommerce_rest_cannot_view
403 wrong-ability token (e.g. magento:admin-only token, or customer token) woocommerce_rest_authorization_required
404 unknown resource id woocommerce_rest_shop_order_invalid_id / woocommerce_rest_product_invalid_id
422 precondition failed (refund non-refundable order, …) woocommerce_rest_invalid_state
429 rate-limit exceeded woocommerce_rest_too_many_requests
500 unexpected server error woocommerce_rest_internal_error

data.status always mirrors the HTTP code. Resource-specific keys (data.id) are added when meaningful.


Rate limiting #

The same Sanctum-token-keyed throttle that protects Magento (api-rest, default 60 req/min) protects WC routes. Configure both globally at /manager/api-settings. When triggered, the response uses the WC error envelope:

{
  "code":    "woocommerce_rest_too_many_requests",
  "message": "Too many requests. Please retry after a short pause.",
  "data":    { "status": 429 }
}

See Magento's Rate limiting section for the full configuration UI walkthrough.


See also #

  • app/Api/Core/OrderRepository.php, app/Api/Core/ProductRepository.php — the abstraction boundary every adapter relies on
  • app/Api/Adapters/WooCommerce/Support/StatusTranslator.php — bidirectional vocabulary mapping
  • WooCommerce REST API reference — the upstream spec this adapter mirrors