Skip to content
Now with GPT-6 Astra support

WooCommerce REST API Adapter

v3 Compatible

Last updated: October 3, 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 date→created_at, title→customer_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 title→name, slug→slug

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) eur → EUR
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) configurable ↔ variable
status status (translated) active ↔ publish
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