跳至主要內容

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 會比對 emailname 欄位其中之一。

請求

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,上限 500
  • currentPage 預設為 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 — 訂單處於不允許取消的狀態(例如 deliveredrefunded

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.comment
  • 404 — 訂單 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 串接

範例——statuspaidshipped,且 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 接受 ASCDESC(預設 ASC)。多組排序由左至右依序套用。

分頁

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

回應會回報實際套用的數值:

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

欄位對應(Magento ↔ STOAR) #

轉接器只對外開放 Magento 的欄位名稱。每個名稱在內部都對應到一個 STOAR 欄位或計算值。不在此清單中的欄位無法用於 filter_groupssortOrders——嘗試使用會回傳 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_paidtotal_refunded — 由 OrderPayment 資料列加上 refunded_amount 推算
  • customer_is_guestcustomer_id === null
  • items[]billing_addressshipping_addresspaymentstatus_histories[]
  • 所有 base_* 合計 — STOAR 為單一幣別,因此 base_grand_total === grand_total

state 的推導

STOAR status Magento state
pending new
paidprocessingshippeddelivered 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_paidpayment.base_amount_paid 0 這筆訂單的 statuspaid,但它的 OrderPayment 稽核記錄是空的。轉接器把 total_paid 算成未封存且成功的 OrderPayment 資料列總和——它不會單憑訂單狀態推斷付款金額。早於付款稽核功能的訂單即使已付款,這裡仍會回傳 0
payment.method "payid" 這是 STOAR 的 gateway key,不是 Magento 的標準方法名稱。可能的值包括 stripebank_transfercash_on_deliverypayidinvoice,以及任何透過 GatewayRegistry 註冊的自訂金流商。
status_histories [] 狀態記錄是在部分舊訂單建立之後才導入的,因此較舊的訂單會回傳空陣列。新建立的訂單一定至少有一筆記錄(初始的 pending 狀態)。
qty_invoicedqty_shippedqty_refundedqty_canceled 所有項目皆為 0 STOAR 沒有獨立的 Invoice/Shipment 實體,因此逐項的出貨計數器一律回報為 0。請改用訂單層級的 total_refundedstatus 欄位。
coupon_code null 只有在下單時使用了折扣碼才會有值。與透過購物車規則套用的優惠無關。
customer_id + customer_is_guest: false 兩者皆有值 表示這筆訂單由已登入的顧客下單。訪客訂單會回傳 customer_id: nullcustomer_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." }(除錯模式會顯示真實訊息)

延伸閱讀 #