Magento REST API 轉接器
可直接替換的 Magento 2 相容介面
最後更新:2026年9月5日
一套可直接替換的 STOAR REST API,完整比照 Magento 2 的線路格式——相同的 URL 路徑、相同的 searchCriteria[…] 查詢語法、相同的回應欄位名稱、相同的驗證流程。既有的 Magento 用戶端函式庫不需修改即可與 STOAR 溝通。
本轉接器是可擴充的 API 轉接器框架 的第一個實作:同一份資料以不同廠商風格對外開放(目前是 Magento,之後還有 Shopify/WooCommerce/自訂轉接器),而不必重複實作訂單商務邏輯。
目錄 #
快速開始 #
# 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
驗證 #
轉接器在背後使用 Laravel Sanctum 個人存取權杖,但對外流程完全遵循 Magento 的約定:POST 憑證、取得一段權杖字串,再以 Authorization: Bearer … 送出。
| 權杖類型 | 端點 | TTL | 能力 | 使用情境 |
|---|---|---|---|---|
| 管理員 | POST /rest/V1/integration/admin/token |
4 小時 | magento:admin |
後端作業(列出訂單、退款、取消) |
| 顧客 | POST /rest/V1/integration/customer/token |
1 小時 | magento:customer |
商店前台整合 |
所有 /rest/V1/orders/* 端點都需要 magento:admin。以顧客權杖存取這些端點會回傳 403。
權杖格式
權杖端點成功時,會以 JSON 字串純量的形式回傳原始權杖——完全照搬 Magento 的約定:
HTTP/1.1 200 OK
Content-Type: application/json
"abc123def456…"
在後續請求中使用它:
Authorization: Bearer abc123def456…
權杖儲存在 personal_access_tokens(Sanctum 的標準資料表),可隨時刪除對應的資料列來撤銷,或在程式中呼叫 $user->tokens()->delete()。
端點 #
所有路徑都相對於 /rest/V1(原封不動沿用 Magento 前綴)。
POST /rest/V1/integration/admin/token
以 admin_users 的憑證核發管理員權杖。username 會比對 email 或 name 欄位其中之一。
請求
POST /rest/V1/integration/admin/token
Content-Type: application/json
{ "username": "[email protected]", "password": "secret" }
回應(200)
"3|EsTmpYnjA…"
錯誤
400— 憑證錯誤、使用者未啟用,或缺少欄位
POST /rest/V1/integration/customer/token
以 stoar_shop_customers 的憑證核發顧客權杖。username 就是電子郵件。
請求
POST /rest/V1/integration/customer/token
Content-Type: application/json
{ "username": "[email protected]", "password": "secret" }
回應(200) — 結構與管理員權杖相同。
錯誤
400— 電子郵件/密碼錯誤、顧客未啟用、缺少欄位
GET /rest/V1/orders
列出訂單,完整支援 Magento 的 searchCriteria[…] 查詢——見下方搜尋條件查詢語法。
Authorization: Bearer 管理員權杖。
回應(200)
{
"items": [
{ /* full order object — see Response shape */ }
],
"search_criteria": {
"filter_groups": [...],
"sort_orders": [...],
"page_size": 20,
"current_page": 1
},
"total_count": 150
}
預設值
pageSize預設為 20,上限 500currentPage預設為 1- 未指定篩選條件 → 回傳所有訂單,最新的在前(依
id DESC排序)
GET /rest/V1/orders/{id}
取得單一訂單及其所有關聯:商品項目、付款、狀態記錄、帳單/配送地址。
Authorization: Bearer 管理員權杖。
回應(200) — 見回應結構。
錯誤
404—{ "message": "No such entity with %fieldName = %fieldValue", "parameters": ["entity_id", "99999"] }
POST /rest/V1/orders/{id}/cancel
取消一筆待付款或已付款的訂單。觸發與管理後台介面相同的流程:將狀態標記為 cancelled、寫入一筆狀態記錄、釋放庫存保留(當訂單上有 session_id 時)。
Authorization: Bearer 管理員權杖。
請求: 空的內容。
回應(200) — true
錯誤
404— 訂單 id 不存在422— 訂單處於不允許取消的狀態(例如delivered、refunded)
GET /rest/V1/orders/{id}/comments
列出訂單的狀態記錄(Magento 稱之為「comments」)。
Authorization: Bearer 管理員權杖。
回應(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
附加一筆狀態記錄留言,但不改變訂單狀態。
Authorization: Bearer 管理員權杖。
請求
{
"statusHistory": {
"comment": "Customer phoned to confirm delivery slot",
"is_customer_notified": false,
"is_visible_on_front": false
}
}
statusHistory.status 是選填的——若省略,會保留現有的訂單狀態。
回應(200) — true
錯誤
400— 缺少statusHistory.comment404— 訂單 id 不存在
POST /rest/V1/order/{id}/refund
注意 — Magento 使用單數形式
/order/,而非/orders/。轉接器完全比照。
發出退款。STOAR 會委派給既有的 Order::processRefund(),它透過 StripeService::processRefund() 呼叫 Stripe。訂單會被標記為 refunded(全額),或維持在目前的已付款狀態並累加 refunded_amount(部分退款)。
Authorization: Bearer 管理員權杖。
請求——全額退款(省略內容或 arguments):
{}
請求——指定金額:
{ "arguments": { "amount": 50.00 } }
請求——逐項退款(Magento 風格):
{
"items": [
{ "order_item_id": 456, "qty": 1 },
{ "order_item_id": 457, "qty": 2 }
]
}
提供 items[] 時,退款金額會依每個項目儲存的 price * qty 計算。若同時給了 arguments.amount,則以它為準。
回應(200) — credit memo id(整數)。STOAR 沒有獨立的 credit memo 實體,因此以訂單 id 代替回傳。
錯誤
404— 訂單 id 不存在422— 訂單不可退款(沒有 Stripe payment intent,或狀態不是 paid/processing/shipped/delivered)
搜尋條件查詢語法 #
GET /rest/V1/orders 端點接受完整的 Magento search criteria 語法。
結構
searchCriteria[filter_groups][N][filters][M][field|value|condition_type]
searchCriteria[sortOrders][N][field|direction]
searchCriteria[pageSize]
searchCriteria[currentPage]
篩選邏輯
- 同一個
filter_groups[N]內的篩選條件以 OR 串接 - 不同的
filter_groups[N]之間以 AND 串接
範例——status 為 paid 或 shipped,且 customer_email 包含 @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
支援的運算子
condition_type |
意義 | 範例 |
|---|---|---|
eq |
等於 | value=paid&condition_type=eq |
neq |
不等於 | value=cancelled&condition_type=neq |
gt |
大於 | value=100&condition_type=gt |
gteq |
≥ | value=2026-01-01&condition_type=gteq |
lt |
小於 | |
lteq |
≤ | |
from |
範圍起點(gteq 的別名) |
|
to |
範圍終點(lteq 的別名) |
|
like |
SQL LIKE;% 萬用字元由您自行提供 |
value=%25%40example.com&condition_type=like |
in |
以逗號分隔的清單 | value=paid,shipped,delivered&condition_type=in |
nin |
NOT IN | |
null |
IS NULL | (不需要 value) |
notnull |
IS NOT NULL | |
finset |
盡力而為的子字串比對 |
不支援的運算子或未知欄位 → 回傳 400,並附上說明用的 message。
排序
searchCriteria[sortOrders][0][field]=created_at
searchCriteria[sortOrders][0][direction]=DESC
searchCriteria[sortOrders][1][field]=grand_total
searchCriteria[sortOrders][1][direction]=ASC
direction 接受 ASC 或 DESC(預設 ASC)。多組排序由左至右依序套用。
分頁
searchCriteria[pageSize]=25 # max 500
searchCriteria[currentPage]=2 # 1-indexed
回應會回報實際套用的數值:
"search_criteria": { "page_size": 25, "current_page": 2 }
欄位對應(Magento ↔ STOAR) #
轉接器只對外開放 Magento 的欄位名稱。每個名稱在內部都對應到一個 STOAR 欄位或計算值。不在此清單中的欄位無法用於 filter_groups 或 sortOrders——嘗試使用會回傳 400。
可篩選/可排序
| Magento 欄位 | STOAR 來源 | 備註 |
|---|---|---|
entity_id |
id |
|
increment_id |
id |
篩選時視為數值(ORD- 前綴僅用於顯示) |
status |
status |
|
state |
status |
STOAR 將 Magento 的 state 併入 status |
customer_id |
customer_id |
|
customer_email |
customer_email |
|
customer_firstname |
customer_info->first_name |
透過 MariaDB JSON_EXTRACT 取得 |
customer_lastname |
customer_info->last_name |
同上 |
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 |
僅供讀取(唯讀欄位)
這些欄位會出現在 JSON 回應中,但不能用於篩選或排序:
total_paid、total_refunded— 由OrderPayment資料列加上refunded_amount推算customer_is_guest—customer_id === nullitems[]、billing_address、shipping_address、payment、status_histories[]- 所有
base_*合計 — STOAR 為單一幣別,因此base_grand_total === grand_total
state 的推導
STOAR status |
Magento state |
|---|---|
pending |
new |
paid、processing、shipped、delivered |
processing |
cancelled |
canceled |
refunded |
closed |
回應結構 #
一筆完整的訂單回應(為求簡潔已截短):
{
"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
}
}
實際範例 #
以下是對正式環境送出 GET /rest/V1/orders/10126 所擷取的實際回應——一筆已付款、金額 USD $936.98、含兩個簡單商品項目的訂單。個資(電子郵件、電話、lookup token、確切街道地址)已匿名化,其餘皆為原文。
請求
GET /rest/V1/orders/10126
Authorization: Bearer 1|vYuVLH4wvkSFfhwAVHGhzVMkOVbKkw8S5gKjVU5o9212c81b
回應(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
}
}
這個範例透露了哪些約定細節
這份真實回應中有幾點值得特別點出,因為它們可能讓整合人員感到意外:
| 欄位 | 觀察到的值 | 為什麼看起來「不對」 |
|---|---|---|
total_paid、payment.base_amount_paid |
0 |
這筆訂單的 status 是 paid,但它的 OrderPayment 稽核記錄是空的。轉接器把 total_paid 算成未封存且成功的 OrderPayment 資料列總和——它不會單憑訂單狀態推斷付款金額。早於付款稽核功能的訂單即使已付款,這裡仍會回傳 0。 |
payment.method |
"payid" |
這是 STOAR 的 gateway key,不是 Magento 的標準方法名稱。可能的值包括 stripe、bank_transfer、cash_on_delivery、payid、invoice,以及任何透過 GatewayRegistry 註冊的自訂金流商。 |
status_histories |
[] |
狀態記錄是在部分舊訂單建立之後才導入的,因此較舊的訂單會回傳空陣列。新建立的訂單一定至少有一筆記錄(初始的 pending 狀態)。 |
qty_invoiced、qty_shipped、qty_refunded、qty_canceled |
所有項目皆為 0 |
STOAR 沒有獨立的 Invoice/Shipment 實體,因此逐項的出貨計數器一律回報為 0。請改用訂單層級的 total_refunded 與 status 欄位。 |
coupon_code |
null |
只有在下單時使用了折扣碼才會有值。與透過購物車規則套用的優惠無關。 |
customer_id + customer_is_guest: false |
兩者皆有值 | 表示這筆訂單由已登入的顧客下單。訪客訂單會回傳 customer_id: null 與 customer_is_guest: true。 |
weight |
0 |
STOAR 不追蹤每筆訂單的重量;此欄位僅為相容 Magento 用戶端而保留。 |
store_id |
1 |
STOAR 為單一商店,此欄位永遠是 1。 |
extension_attributes.lookup_token |
已遮蔽 | 這是 STOAR 每筆訂單專屬的「magic link」權杖,用於訪客查詢訂單狀態的頁面。切勿在用戶端程式碼或公開記錄中曝露——只要持有這個權杖,不需驗證即可檢視該訂單。 |
錯誤結構 #
每個 /rest/V1/* 錯誤都遵循 Magento 的封裝格式:
{
"message": "Human message with %fieldName placeholders",
"parameters": ["fieldName", "fieldValue"],
"trace": "…stack trace…"
}
parameters 會對應到 message 中的 %1、%2 或 %fieldName 佔位符,讓在地化的用戶端可以代入實際值。
trace 只有在 APP_DEBUG=true 時才會包含。
| HTTP | 觸發原因 | 範例 |
|---|---|---|
| 400 | 輸入有誤——未知的篩選欄位、不支援的 condition_type、驗證失敗 |
{ "message": "Unsupported condition_type: zorp" } |
| 401 | 缺少權杖或權杖無效 | { "message": "Consumer is not authorized to access %resources", "parameters": ["Magento_Sales::sales"] } |
| 403 | 權杖能力不符(例如以顧客權杖存取管理員端點) | { "message": "The consumer does not have access to the requested resource." } |
| 404 | 訂單 id 不存在 | { "message": "No such entity with %fieldName = %fieldValue", "parameters": ["entity_id", "99999"] } |
| 422 | 前置條件不符(取消不可取消的訂單、退款不可退款的訂單) | { "message": "Order 5 cannot be cancelled in status 'delivered'." } |
| 500 | 非預期的伺服器錯誤 | { "message": "Internal server error." }(除錯模式會顯示真實訊息) |
延伸閱讀 #
- Adobe Commerce REST 參考文件 — 本轉接器所比照的上游規格