Skip to content

Magento REST API Adapter

Drop-in Magento 2 Compatible

Last updated: May 15, 2026

A drop-in compatible REST API for STOAR that mirrors Magento 2's wire format — same URL paths, same searchCriteria[…] query syntax, same response field names, same auth flow. Existing Magento client libraries can talk to STOAR without modification.

The adapter is the first implementation of an extensible API adapter framework: the same data is exposed under different vendor flavours (Magento now, Shopify / WooCommerce / custom adapters later) without duplicating order business logic.


Table of contents #


Quick start #

# 1. Get an admin token
TOKEN=$(curl -s -X POST https://app.stoar.ai/rest/V1/integration/admin/token \
  -H 'Content-Type: application/json' \
  -d '{"username":"[email protected]","password":"your-password"}' \
  | tr -d '"')

# 2. List the most recent 5 orders
curl -s "https://app.stoar.ai/rest/V1/orders?searchCriteria[pageSize]=5" \
  -H "Authorization: Bearer $TOKEN" \
  | jq '.items[] | {entity_id, increment_id, status, grand_total}'

# 3. Fetch a single order
curl -s "https://app.stoar.ai/rest/V1/orders/123" \
  -H "Authorization: Bearer $TOKEN" \
  | jq

Authentication #

The adapter uses Laravel Sanctum personal access tokens behind the scenes, but the user-facing flow follows Magento's contract exactly: POST credentials, receive a token string, send it as Authorization: Bearer ….

Token type Endpoint TTL Ability Use case
Admin POST /rest/V1/integration/admin/token 4 h magento:admin Backend operations (list orders, refund, cancel)
Customer POST /rest/V1/integration/customer/token 1 h magento:customer Storefront integrations

All /rest/V1/orders/* endpoints require magento:admin. A customer token presented to those endpoints returns 403.

Token format

A successful token endpoint returns the raw token as a JSON-quoted scalar — the literal Magento contract:

HTTP/1.1 200 OK
Content-Type: application/json

"abc123def456…"

Use it on subsequent requests:

Authorization: Bearer abc123def456…

Tokens are stored in personal_access_tokens (Sanctum's standard table) and can be revoked at any time by deleting the corresponding row, or via $user->tokens()->delete() in code.


Endpoints #

All paths are relative to /rest/V1 (verbatim Magento prefix).

POST /rest/V1/integration/admin/token

Issue an admin token from admin_users credentials. The username matches against either the email or name column.

Request

POST /rest/V1/integration/admin/token
Content-Type: application/json

{ "username": "[email protected]", "password": "secret" }

Response (200)

"3|EsTmpYnjA…"

Errors

  • 400 — invalid credentials, inactive user, or missing fields

POST /rest/V1/integration/customer/token

Issue a customer token from stoar_shop_customers credentials. Username is the email.

Request

POST /rest/V1/integration/customer/token
Content-Type: application/json

{ "username": "[email protected]", "password": "secret" }

Response (200) — same shape as admin token.

Errors

  • 400 — invalid email/password, inactive customer, missing fields

GET /rest/V1/orders

List orders with full Magento searchCriteria[…] query support — see Search criteria query syntax below.

Authorization: Bearer admin token.

Response (200)

{
  "items": [
    { /* full order object — see Response shape */ }
  ],
  "search_criteria": {
    "filter_groups": [...],
    "sort_orders":   [...],
    "page_size":     20,
    "current_page":  1
  },
  "total_count": 150
}

Defaults

  • pageSize defaults to 20, capped at 500
  • currentPage defaults to 1
  • No filters → all orders, newest first (sorted by id DESC)

GET /rest/V1/orders/{id}

Fetch a single order with all relations: items, payments, status histories, billing/shipping addresses.

Authorization: Bearer admin token.

Response (200) — see Response shape.

Errors

  • 404{ "message": "No such entity with %fieldName = %fieldValue", "parameters": ["entity_id", "99999"] }

POST /rest/V1/orders/{id}/cancel

Cancel a pending or paid order. Triggers the same flow as the admin UI: marks status cancelled, writes a status-history row, releases inventory reservations (when session_id is present on the order).

Authorization: Bearer admin token.

Request: empty body.

Response (200)true

Errors

  • 404 — unknown order id
  • 422 — order is in a state that disallows cancellation (e.g. delivered, refunded)

GET /rest/V1/orders/{id}/comments

List the order's status history (Magento calls these "comments").

Authorization: Bearer admin token.

Response (200)

{
  "items": [
    {
      "entity_id":            42,
      "parent_id":            123,
      "comment":              "Payment confirmed",
      "status":               "paid",
      "created_at":           "2026-04-10T12:00:00+00:00",
      "is_customer_notified": false,
      "is_visible_on_front":  false,
      "extension_attributes": { "old_status": "pending", "changed_by": "system" }
    }
  ],
  "search_criteria": { "filter_groups": [], "sort_orders": [], "page_size": 1, "current_page": 1 },
  "total_count":     1
}

POST /rest/V1/orders/{id}/comments

Append a status-history comment without changing the order's status.

Authorization: Bearer admin token.

Request

{
  "statusHistory": {
    "comment":              "Customer phoned to confirm delivery slot",
    "is_customer_notified": false,
    "is_visible_on_front":  false
  }
}

statusHistory.status is optional — if omitted, the existing order status is preserved.

Response (200)true

Errors

  • 400 — missing statusHistory.comment
  • 404 — unknown order id

POST /rest/V1/order/{id}/refund

Note — Magento uses the singular form /order/, NOT /orders/. The adapter mirrors this exactly.

Issue a refund. STOAR delegates to the existing Order::processRefund() which calls Stripe via StripeService::processRefund(). Order is marked refunded (full) or stays in its current paid state with refunded_amount accumulated (partial).

Authorization: Bearer admin token.

Request — full refund (omit body or arguments):

{}

Request — explicit amount:

{ "arguments": { "amount": 50.00 } }

Request — per-item (Magento style):

{
  "items": [
    { "order_item_id": 456, "qty": 1 },
    { "order_item_id": 457, "qty": 2 }
  ]
}

When items[] is provided, the refund amount is computed from each item's stored price * qty. arguments.amount (when given) overrides that calculation.

Response (200) — credit memo id (integer). STOAR has no separate credit-memo entity, so the order id is returned as a stand-in.

Errors

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

Search criteria query syntax #

The GET /rest/V1/orders endpoint accepts the full Magento search-criteria grammar.

Structure

searchCriteria[filter_groups][N][filters][M][field|value|condition_type]
searchCriteria[sortOrders][N][field|direction]
searchCriteria[pageSize]
searchCriteria[currentPage]

Filter logic

  • Filters within the same filter_groups[N] are joined by OR
  • Different filter_groups[N] are joined by AND

Example — orders with status of paid OR shipped, AND customer_email containing @example.com:

GET /rest/V1/orders
  ?searchCriteria[filter_groups][0][filters][0][field]=status
  &searchCriteria[filter_groups][0][filters][0][value]=paid
  &searchCriteria[filter_groups][0][filters][0][condition_type]=eq
  &searchCriteria[filter_groups][0][filters][1][field]=status
  &searchCriteria[filter_groups][0][filters][1][value]=shipped
  &searchCriteria[filter_groups][0][filters][1][condition_type]=eq
  &searchCriteria[filter_groups][1][filters][0][field]=customer_email
  &searchCriteria[filter_groups][1][filters][0][value]=%[email protected]
  &searchCriteria[filter_groups][1][filters][0][condition_type]=like

Supported operators

condition_type Meaning Example
eq equals value=paid&condition_type=eq
neq not equal value=cancelled&condition_type=neq
gt greater than value=100&condition_type=gt
gteq value=2026-01-01&condition_type=gteq
lt less than
lteq
from range start (alias for gteq)
to range end (alias for lteq)
like SQL LIKE; you supply % wildcards value=%25%40example.com&condition_type=like
in comma-separated list value=paid,shipped,delivered&condition_type=in
nin NOT IN
null IS NULL (no value needed)
notnull IS NOT NULL
finset best-effort substring match

Unsupported operators or unknown fields → 400 with an explanatory message.

Sort orders

searchCriteria[sortOrders][0][field]=created_at
searchCriteria[sortOrders][0][direction]=DESC
searchCriteria[sortOrders][1][field]=grand_total
searchCriteria[sortOrders][1][direction]=ASC

direction accepts ASC or DESC (default ASC). Multiple sort orders compose left-to-right.

Pagination

searchCriteria[pageSize]=25       # max 500
searchCriteria[currentPage]=2     # 1-indexed

The response echoes the actual values applied:

"search_criteria": { "page_size": 25, "current_page": 2 }

Field mapping (Magento ↔ STOAR) #

The adapter only exposes Magento field names. Internally each one maps to a STOAR column or computed value. Fields not in this list cannot be used in filter_groups or sortOrders — attempting to do so returns 400.

Filterable / sortable

Magento field STOAR source Notes
entity_id id
increment_id id filtering treats this as numeric (the ORD- prefix is render-only)
status status
state status STOAR collapses Magento's state into status
customer_id customer_id
customer_email customer_email
customer_firstname customer_info->first_name resolved via MariaDB JSON_EXTRACT
customer_lastname customer_info->last_name same
grand_total total_amount
subtotal subtotal
tax_amount tax_amount
shipping_amount shipping_amount
discount_amount discount_amount
coupon_code coupon_code
currency_code currency
order_currency_code currency
created_at created_at
updated_at updated_at

Response-only (read-out fields)

These appear in the JSON response but cannot be used as filter/sort fields:

  • total_paid, total_refunded — derived from OrderPayment rows + refunded_amount
  • customer_is_guestcustomer_id === null
  • items[], billing_address, shipping_address, payment, status_histories[]
  • All base_* totals — STOAR is single-currency, so base_grand_total === grand_total

State derivation

STOAR status Magento state
pending new
paid, processing, shipped, delivered processing
cancelled canceled
refunded closed

Response shape #

A complete order response (truncated for brevity):

{
  "entity_id":             123,
  "increment_id":          "ORD-000123",
  "state":                 "processing",
  "status":                "paid",

  "customer_id":           45,
  "customer_email":        "[email protected]",
  "customer_firstname":    "Jane",
  "customer_lastname":     "Doe",
  "customer_group_id":     0,
  "customer_is_guest":     false,

  "base_currency_code":    "EUR",
  "currency_code":         "EUR",
  "order_currency_code":   "EUR",

  "grand_total":           115.0,
  "base_grand_total":      115.0,
  "subtotal":              100.0,
  "base_subtotal":         100.0,
  "tax_amount":            10.0,
  "base_tax_amount":       10.0,
  "shipping_amount":       5.0,
  "base_shipping_amount":  5.0,
  "discount_amount":       0.0,
  "base_discount_amount":  0.0,

  "total_paid":            115.0,
  "total_refunded":        0.0,
  "base_total_paid":       115.0,
  "base_total_refunded":   0.0,

  "shipping_description":  "Standard",
  "shipping_incl_tax":     5.0,
  "base_shipping_incl_tax":5.0,

  "created_at":            "2026-04-10T09:00:00+00:00",
  "updated_at":            "2026-04-10T09:30:00+00:00",

  "is_virtual":            false,
  "weight":                0,
  "store_id":              1,
  "coupon_code":           null,

  "items": [
    {
      "item_id":           456,
      "order_id":          123,
      "product_id":        789,
      "product_type":      "simple",
      "sku":               "WID-1-A",
      "name":              "Widget",
      "qty_ordered":       2.0,
      "qty_invoiced":      0.0,
      "qty_shipped":       0.0,
      "qty_refunded":      0.0,
      "qty_canceled":      0.0,
      "price":             50.0,
      "base_price":        50.0,
      "price_incl_tax":    55.0,
      "row_total":         100.0,
      "row_total_incl_tax":110.0,
      "tax_amount":        10.0,
      "tax_percent":       10.0,
      "discount_amount":   0,
      "extension_attributes": { "variant_id": 12 }
    }
  ],

  "billing_address": {
    "entity_id":     null,
    "parent_id":     123,
    "address_type":  "billing",
    "email":         "[email protected]",
    "firstname":     "Jane",
    "lastname":      "Doe",
    "street":        "1 Test St",
    "city":          "Berlin",
    "country_id":    "DE",
    "postcode":      "10115",
    "region":        null,
    "telephone":     "+49…"
  },

  "shipping_address": { /* same shape, address_type="shipping" */ },

  "payment": {
    "entity_id":              null,
    "parent_id":              123,
    "method":                 "stripe",
    "base_amount_paid":       115.0,
    "base_amount_refunded":   0.0,
    "cc_trans_id":            "pi_test_…",
    "extension_attributes": {
      "payments": [
        { "id": 1, "gateway": "stripe", "amount": 115.0, "currency": "eur",
          "status": "succeeded", "reference": "pi_test_…", "archived_at": null,
          "created_at": "2026-04-10T09:05:00+00:00" }
      ]
    }
  },

  "status_histories": [
    {
      "entity_id":             1,
      "parent_id":             123,
      "comment":               null,
      "status":                "pending",
      "created_at":            "2026-04-10T09:00:00+00:00",
      "extension_attributes":  { "old_status": null, "changed_by": "System" }
    },
    {
      "entity_id":             2,
      "parent_id":             123,
      "comment":               "Payment confirmed via webhook",
      "status":                "paid",
      "created_at":            "2026-04-10T09:05:00+00:00",
      "extension_attributes":  { "old_status": "pending", "changed_by": "System" }
    }
  ],

  "extension_attributes": {
    "lookup_token":     "abc…",
    "tracking_number":  null,
    "tracking_url":     null,
    "tracking_carrier": null,
    "shipment_status":  null,
    "admin_notes":      null,
    "customer_notes":   null
  }
}

Real-world example #

Below is a live response captured against production for GET /rest/V1/orders/10126 — a paid USD $936.98 order with two simple-product line items. PII (email, phone, lookup token, exact street address) has been anonymized; everything else is verbatim.

Request

GET /rest/V1/orders/10126
Authorization: Bearer 1|vYuVLH4wvkSFfhwAVHGhzVMkOVbKkw8S5gKjVU5o9212c81b

Response (200)

{
  "entity_id":             10126,
  "increment_id":          "ORD-010126",
  "state":                 "processing",
  "status":                "paid",
  "customer_id":           5794,
  "customer_email":        "[email protected]",
  "customer_firstname":    "Jane",
  "customer_lastname":     "Doe",
  "customer_group_id":     0,
  "customer_is_guest":     false,
  "base_currency_code":    "USD",
  "currency_code":         "USD",
  "order_currency_code":   "USD",
  "grand_total":           936.98,
  "base_grand_total":      936.98,
  "subtotal":              936.98,
  "base_subtotal":         936.98,
  "tax_amount":            0,
  "base_tax_amount":       0,
  "shipping_amount":       0,
  "base_shipping_amount":  0,
  "discount_amount":       0,
  "base_discount_amount":  0,
  "total_paid":            0,
  "total_refunded":        0,
  "base_total_paid":       0,
  "base_total_refunded":   0,
  "shipping_description":  "Free Shipping",
  "shipping_incl_tax":     0,
  "base_shipping_incl_tax":0,
  "created_at":            "2025-06-03T04:56:43+00:00",
  "updated_at":            "2025-06-03T04:56:43+00:00",
  "is_virtual":            false,
  "weight":                0,
  "store_id":              1,
  "coupon_code":           null,
  "items": [
    {
      "item_id":               30219,
      "order_id":              10126,
      "product_id":            112238,
      "product_type":          "simple",
      "sku":                   "RELOOP_TERMINALMIX8_025-DEF",
      "name":                  "Reloop Terminal Mix 8",
      "qty_ordered":           3,
      "qty_invoiced":          0,
      "qty_shipped":           0,
      "qty_refunded":          0,
      "qty_canceled":          0,
      "price":                 299,
      "base_price":            299,
      "price_incl_tax":        299,
      "base_price_incl_tax":   299,
      "original_price":        299,
      "base_original_price":   299,
      "row_total":             897,
      "base_row_total":        897,
      "row_total_incl_tax":    897,
      "base_row_total_incl_tax":897,
      "discount_amount":       0,
      "base_discount_amount":  0,
      "discount_percent":      0,
      "tax_amount":            0,
      "base_tax_amount":       0,
      "tax_percent":           0,
      "amount_refunded":       0,
      "base_amount_refunded":  0,
      "row_weight":            0,
      "created_at":            "2025-06-03T04:56:43+00:00",
      "updated_at":            "2025-06-03T04:56:43+00:00",
      "is_qty_decimal":        false,
      "no_discount":           false,
      "parent_item_id":        null,
      "extension_attributes":  { "variant_id": 95589 }
    },
    {
      "item_id":               30220,
      "order_id":              10126,
      "product_id":            51706,
      "product_type":          "simple",
      "sku":                   "SK8-SOCK-027-DEF",
      "name":                  "Premium Skateboard Socks",
      "qty_ordered":           2,
      "qty_invoiced":          0,
      "qty_shipped":           0,
      "qty_refunded":          0,
      "qty_canceled":          0,
      "price":                 19.99,
      "base_price":            19.99,
      "price_incl_tax":        19.99,
      "base_price_incl_tax":   19.99,
      "original_price":        19.99,
      "base_original_price":   19.99,
      "row_total":             39.98,
      "base_row_total":        39.98,
      "row_total_incl_tax":    39.98,
      "base_row_total_incl_tax":39.98,
      "discount_amount":       0,
      "base_discount_amount":  0,
      "discount_percent":      0,
      "tax_amount":            0,
      "base_tax_amount":       0,
      "tax_percent":           0,
      "amount_refunded":       0,
      "base_amount_refunded":  0,
      "row_weight":            0,
      "created_at":            "2025-06-03T04:56:43+00:00",
      "updated_at":            "2025-06-03T04:56:43+00:00",
      "is_qty_decimal":        false,
      "no_discount":           false,
      "parent_item_id":        null,
      "extension_attributes":  { "variant_id": 33857 }
    }
  ],
  "billing_address": {
    "entity_id":           null,
    "parent_id":           10126,
    "address_type":        "billing",
    "email":               null,
    "firstname":           "Jane",
    "lastname":            "Doe",
    "middlename":          null,
    "prefix":              null,
    "suffix":              null,
    "street":              "1 Example Street",
    "city":                "Phoenix",
    "country_id":          "US",
    "postcode":            "85001",
    "region":              "AZ",
    "region_code":         "AZ",
    "region_id":           null,
    "telephone":           "+1-555-0100",
    "fax":                 null,
    "company":             null,
    "customer_address_id": null
  },
  "shipping_address": {
    "entity_id":           null,
    "parent_id":           10126,
    "address_type":        "shipping",
    "email":               null,
    "firstname":           "Jane",
    "lastname":            "Doe",
    "middlename":          null,
    "prefix":              null,
    "suffix":              null,
    "street":              "1 Example Street",
    "city":                "Phoenix",
    "country_id":          "US",
    "postcode":            "85001",
    "region":              "AZ",
    "region_code":         "AZ",
    "region_id":           null,
    "telephone":           "+1-555-0100",
    "fax":                 null,
    "company":             null,
    "customer_address_id": null
  },
  "payment": {
    "entity_id":              null,
    "parent_id":              10126,
    "base_amount_authorized": 936.98,
    "base_amount_paid":       0,
    "base_amount_refunded":   0,
    "base_shipping_amount":   0,
    "base_shipping_captured": 0,
    "base_shipping_refunded": 0,
    "billing_address_id":     null,
    "cc_avs_status":          null,
    "cc_cid_status":          null,
    "cc_exp_month":           null,
    "cc_exp_year":            null,
    "cc_last4":               null,
    "cc_number_enc":          null,
    "cc_owner":               null,
    "cc_status":              null,
    "cc_status_description":  null,
    "cc_trans_id":            null,
    "created_at":             null,
    "updated_at":             null,
    "method":                 "payid",
    "po_number":              null,
    "protection_eligibility": null,
    "quote_payment_id":       null,
    "extension_attributes":   { "payments": [] }
  },
  "status_histories": [],
  "extension_attributes": {
    "lookup_token":      "REDACTED-FOR-DOCS",
    "tracking_number":   null,
    "tracking_url":      null,
    "tracking_carrier":  null,
    "shipment_status":   null,
    "admin_notes":       null,
    "customer_notes":    null
  }
}

What this example tells you about the contract

A few things in this real response are worth flagging because they may surprise integrators:

Field Observed Why it can look "wrong"
total_paid, payment.base_amount_paid 0 The order's status is paid, but its OrderPayment audit log is empty. The adapter computes total_paid as the sum of non-archived succeeded OrderPayment rows — it does not infer payment amount from the order's status alone. Orders predating the payment-audit feature will return 0 here even though they are paid.
payment.method "payid" This is the STOAR gateway-key, not a Magento-canonical method name. Possible values include stripe, bank_transfer, cash_on_delivery, payid, invoice, plus any custom gateway registered via GatewayRegistry.
status_histories [] Status logs were introduced after some legacy orders were created, so older orders return an empty array. Newly-created orders always have at least one entry (the initial pending status).
qty_invoiced, qty_shipped, qty_refunded, qty_canceled 0 for all items STOAR has no separate Invoice/Shipment entities, so per-item fulfilment counters are always reported as 0. Use the order-level total_refunded and status fields instead.
coupon_code null Only populated when the order was placed with a discount code. Unrelated to coupons applied via cart-rules.
customer_id + customer_is_guest: false both populated When the order was placed by a logged-in customer. Guest orders return customer_id: null and customer_is_guest: true.
weight 0 STOAR does not track per-order weight; the field is included for Magento client compatibility only.
store_id 1 STOAR is single-store; this field is always 1.
extension_attributes.lookup_token redacted This is STOAR's per-order "magic-link" token used for guest order-status pages. Never expose it in client-side code or public logs — possession of the token is sufficient to view the order without authentication.

Error shape #

Every /rest/V1/* error follows the Magento envelope:

{
  "message":    "Human message with %fieldName placeholders",
  "parameters": ["fieldName", "fieldValue"],
  "trace":      "…stack trace…"
}

parameters lines up with the %1, %2, or %fieldName placeholders in message so localised clients can substitute values. trace is included only when APP_DEBUG=true.

HTTP Trigger Example
400 bad input — unknown filter field, unsupported condition_type, validation failure { "message": "Unsupported condition_type: zorp" }
401 missing or invalid token { "message": "Consumer is not authorized to access %resources", "parameters": ["Magento_Sales::sales"] }
403 wrong-ability token (e.g. customer token on admin endpoint) { "message": "The consumer does not have access to the requested resource." }
404 unknown order id { "message": "No such entity with %fieldName = %fieldValue", "parameters": ["entity_id", "99999"] }
422 precondition failed (cancel non-cancellable, refund non-refundable) { "message": "Order 5 cannot be cancelled in status 'delivered'." }
500 unexpected server error { "message": "Internal server error." } (debug mode shows real message)

See also #