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
- Authentication
- Endpoints
- Search criteria query syntax
- Field mapping (Magento ↔ STOAR)
- Response shape
- Real-world example
- Error shape
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
pageSizedefaults to 20, capped at 500currentPagedefaults 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 id422— 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— missingstatusHistory.comment404— 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 id422— 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 fromOrderPaymentrows +refunded_amountcustomer_is_guest—customer_id === nullitems[],billing_address,shipping_address,payment,status_histories[]- All
base_*totals — STOAR is single-currency, sobase_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 #
- Adobe Commerce REST reference — the upstream spec this adapter mirrors