# 一次詢價與下單

本專題依整合的建構順序逐個請求地介紹 Uniorder API（`/api/v1/uniorder/...`）：登入、詢價、依所選 `rate_id` 下單、列印面單、查詢、追蹤和取消訂單，以及批次處理貨件。一次詢價即列出帳號寄送包裹的所有方式：商戶自有配送，以及依請求回傳的每項面單承運商服務。使用 `rate_id` 下單即為該服務建立訂單：配送訂單，或依所詢承運商服務購買面單的面單訂單。本專題面向透過商戶帳號出貨的網路商店、訂單管理系統和 ERP 的開發人員。

## 1. 可以建構的應用

以下範例均以同一家商戶為例：**Fleurs du Plateau**，一家位於 4500 Rue Saint-Denis, Montreal (H2J 2L3) 的花店，在線上銷售花束。一個典型包裹是一個 1.2 kg、40 × 25 × 25 cm 的盒子，寄給位於 6841 Rue Saint-Denis, Montreal (H2S 2S3) 的 Jane Recipient，網路商店訂單號為 `WEB-10045`。

- **提供全部運送選項的結帳頁面。** 網路商店對包裹詢價一次，將當日本地配送與帳號的每項承運商面單服務並列顯示，每項附帶價格，然後依客戶所選的選項建立訂單。
- **自動列印面單。** 訂單建立後，網路商店下載面單 PDF 並傳送到包裝工作站的印表機，無論包裹由商戶配送還是由承運商運送。
- **帶即時追蹤的訂單頁面。** 客戶的訂單頁面顯示貨件的狀態和事件時間軸，花束送達後還顯示簽收憑證。
- **來自 ERP 的夜間批次處理。** 當天的批發訂單在一個最多 500 行的排隊任務中完成詢價和建立，每筆結果依 `reference` 與其訂單行對應。

## 2. 本專題的適用範圍

本專題是 Uniorder API 的逐步操作指南。Uniorder 提供的功能及其原因見 **Uniorder：所有貨件統一一個介面**；本專題提供每個呼叫的請求、回應和檢查項。

對於透過本地配送或承運商面單寄送包裹的新整合，建議以 Uniorder 作為統一入口：它以一種請求結構取代對本地配送 API 和承運商面單 API 的分別呼叫。**自有車隊取件與配送** 和 **承運商打單** 中介紹的原有介面繼續可用且保持不變。Uniorder 不適用於客戶帳號預訂的運送服務，也不適用於倉儲與出庫訂單；這兩類請使用 **運送服務** 和 **倉儲與出庫**。

## 3. 開始之前

- **帳號。** 使用具有 API 權限的商戶（客戶）帳號或該商戶的員工帳號。商戶的客戶帳號也可以呼叫 Uniorder，且始終以其自身身分詢價和計費。建立配送訂單需要下單權限。
- **客戶。** 商戶帳號或員工帳號可以在詢價中透過 `customer_id` 或 `customer_code` 為其某個客戶詢價和下單；此時 `rate_id` 帶有該客戶，價格依該客戶的價格方案計算。
- **面單服務。** 要取得 `label_service` 報價，帳號（或指定的客戶）至少需要設定一個面單承運商帳號。
- **測試資料。** 對於 `self_delivery` 報價，請使用商戶配送區域內的地址，並使用 `WEB-10045` 等之後可取消的測試參考號。
- **權杖。** 從您的伺服器請求存取權杖並保存在伺服器上。切勿將其傳送到瀏覽器或行動應用程式。
- **預留位置。** 將 `YOUR_HOST` 替換為您所在環境的 API 主機，將 `ACCESS_TOKEN` 替換為第 4 步取得的權杖。

## 4. 登入

每個 Uniorder 呼叫都以某個帳號的身分發起。請從伺服器登入一次，保存回傳的權杖，並在每個請求中攜帶。

**REST：** `POST /api/v1/user/login` — [REST 手冊](/api/documentation#/paths/v1-user-login/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/user/login \
  -H "Content-Type: application/json" \
  -d '{"email":"orders@fleursduplateau.example","password":"your_password"}'
```

```json
{
  "result": true,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
}
```

- `access_token`：將其置於後續每個請求的請求標頭中：

```
Authorization: Bearer ACCESS_TOKEN
```

GraphQL 請求提交至 `POST /api/graphql`，並使用相同的請求標頭。

**GraphQL：** `userLogin`（[GraphQL 手冊](/api/graphql/documentation#/user/userLogin)）

**驗證：** 登入成功後回傳 `access_token`。後續請求若未攜帶該權杖，將回傳 `401`。

## 5. 對所有服務詢價

詢價列出包裹的所有寄送方式，每項附帶價格和 `rate_id`。結帳頁面將這些報價作為選項顯示；不會建立或預訂任何內容。

**REST：** `POST /api/v1/uniorder/rate` — [REST 手冊](/api/documentation#/paths/v1-uniorder-rate/post)

寄件人和收件人均須為完整地址；只有 `from_address_2` 和 `to_address_2` 為可選。每個包裹都需要 `weight`、`length`、`width` 和 `height`。將 `quote_labels` 設為 `true` 可加入面單承運商服務；此時兩端的姓名和電話均為必填。商戶帳號或員工帳號可以透過 `customer_id` 或 `customer_code` 為其某個客戶詢價。當價格取決於送貨時段時，會考慮時段（`time_window_start`、`time_window_end`，格式為 `YYYY-MM-DD HH:MM:SS`）。

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/rate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "D",
    "from_name": "Fleurs du Plateau",
    "from_telephone": "5145550100",
    "from_address": "4500 Rue Saint-Denis",
    "from_city": "Montreal",
    "from_province": "QC",
    "from_country": "CA",
    "from_postcode": "H2J2L3",
    "to_name": "Jane Recipient",
    "to_telephone": "5145550199",
    "to_address": "6841 Rue Saint-Denis",
    "to_address_2": "Apt 2",
    "to_city": "Montreal",
    "to_province": "QC",
    "to_country": "CA",
    "to_postcode": "H2S2S3",
    "time_window_start": "2026-10-02 13:00:00",
    "time_window_end": "2026-10-02 17:00:00",
    "quote_labels": true,
    "packages": [{
      "weight": 1.2,
      "weight_unit": 2,
      "length": 40,
      "width": 25,
      "height": 25,
      "dimension_unit": 2
    }]
  }'
```

`weight_unit`：`1` 公克，`2` 公斤，`3` 盎司，`4` 磅。`dimension_unit`：`1` 公釐，`2` 公分，`3` 公尺，`4` 英寸。

```json
{
  "result": true,
  "rates": [
    {
      "rate_id": "eyJpdiI6Ik1rT2Z...",
      "type": "self_delivery",
      "channel_id": null,
      "shipping_price": "14.60",
      "currency": "CAD",
      "price_details": { "shipping_fee": 12.92, "sub_total": "12.92" },
      "warning": null
    },
    {
      "rate_id": "eyJpdiI6IlpxR0...",
      "type": "label_service",
      "shipping_price": "18.40",
      "currency": "CAD",
      "shipping_method_id": 72,
      "shipping_method_name": "UPS",
      "carrier_name": "ups",
      "service_code": "ups_standard",
      "service_name": "UPS STANDARD",
      "transit_days": 3
    }
  ],
  "errors": []
}
```

- `type` `self_delivery`：由商戶配送。每次詢價最多一條。
- `type` `label_service`：每個面單帳號的每項服務各一條。請向客戶顯示 `service_name`、`shipping_price` 和 `transit_days`。
- `errors` 列出無法詢價的項目及其 `type`。配送區域外的地址會產生類型為 `self_delivery`、代碼為 `OUT_OF_DELIVERY_AREA` 的錯誤；此時僅顯示面單服務。
- `rate_id` 的有效期為 30 分鐘，且僅對發起詢價的帳號有效。請將其與結帳工作階段一起保存。
- 至少找到一條報價時，`result` 為 `true`。

**GraphQL：** `uniorderRate`（[GraphQL 手冊](/api/graphql/documentation#/orders/uniorderRate)）。回應為 JSON 標量，因此該操作沒有選擇集。

```graphql
mutation QuoteBouquet($packages: [Json]!) {
  uniorderRate(
    type: "D"
    from_name: "Fleurs du Plateau"
    from_telephone: "5145550100"
    from_address: "4500 Rue Saint-Denis"
    from_city: "Montreal"
    from_province: "QC"
    from_country: "CA"
    from_postcode: "H2J2L3"
    to_name: "Jane Recipient"
    to_telephone: "5145550199"
    to_address: "6841 Rue Saint-Denis"
    to_city: "Montreal"
    to_province: "QC"
    to_country: "CA"
    to_postcode: "H2S2S3"
    quote_labels: true
    packages: $packages
  )
}
```

變數：

```json
{ "packages": [{ "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }] }
```

**驗證：** 對區域內地址，`rates` 包含一條 `self_delivery` 報價；使用 `quote_labels` 時，每項承運商服務各有一條 `label_service` 報價。不會建立任何內容。

## 6. 依所選報價建立訂單

客戶付款後，網路商店使用所選選項的 `rate_id` 和相同的貨件資訊建立訂單。由 `rate_id` 決定服務；請求中的其他任何內容都不用於選擇服務。

**REST：** `POST /api/v1/uniorder` — [REST 手冊](/api/documentation#/paths/v1-uniorder/post)

每次建立時都應傳送 `Idempotency-Key` 請求標頭，每張訂單使用唯一值。使用相同的鍵和相同的請求本文重試時，回傳第一次的回應並帶有 `replayed` `true`，不會建立第二張訂單。

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: checkout-10045" \
  -d '{
    "rate_id": "eyJpdiI6Ik1rT2Z...",
    "ref": "WEB-10045",
    "type": "D",
    "from_name": "Fleurs du Plateau",
    "from_telephone": "5145550100",
    "from_address": "4500 Rue Saint-Denis",
    "from_city": "Montreal",
    "from_province": "QC",
    "from_country": "CA",
    "from_postcode": "H2J2L3",
    "to_name": "Jane Recipient",
    "to_telephone": "5145550199",
    "to_email": "jane@example.com",
    "to_address": "6841 Rue Saint-Denis",
    "to_address_2": "Apt 2",
    "to_city": "Montreal",
    "to_province": "QC",
    "to_country": "CA",
    "to_postcode": "H2S2S3",
    "time_window_start": "2026-10-02 13:00:00",
    "time_window_end": "2026-10-02 17:00:00",
    "delivery_instruction": "Ring the bell at the side door.",
    "packages": [{
      "ref": "WEB-10045-1",
      "weight": 1.2,
      "weight_unit": 2,
      "length": 40,
      "width": 25,
      "height": 25,
      "dimension_unit": 2
    }]
  }'
```

`self_delivery` 報價建立配送訂單。對於 `type` `D`，收件人為停靠點；將 `need_pick_up` 設為 `1` 可安排在寄件人處取件。對於 `type` `P`，寄件人為停靠點。

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "ref": "WEB-10045",
  "tracking_numbers": ["SR26092800001"],
  "shipping_price": "14.60",
  "quoted_price": "14.60",
  "currency": "CAD"
}
```

`label_service` 報價建立面單訂單，並依所詢承運商服務購買面單。`type` 必須為 `D`，且 `from_name`、`from_telephone`、`to_name` 和 `to_telephone` 為必填。若客戶選擇的是 UPS STANDARD，回應將為：

```json
{
  "result": true,
  "type": "label_service",
  "id": 123457,
  "ref": "WEB-10045",
  "tracking_numbers": ["SR26092800002"],
  "shipping_price": "18.40",
  "quoted_price": "18.40",
  "currency": "CAD",
  "label": {
    "carrier_name": "ups",
    "service_code": "ups_standard",
    "main_tracking_number": "1Z999AA10123456784",
    "label_status": "ready",
    "shipping_label": "JVBERi0xLjQK..."
  }
}
```

- `id`：請與網路商店訂單一起保存；之後的每個呼叫都使用它。
- `tracking_numbers`：貨件自身的運單號，每個包裹一個。
- `shipping_price`：實際收取的價格。訂單在建立時定價；`quoted_price` 為詢價時的價格。兩者可能不同。
- `label.main_tracking_number` 和 `label.shipping_label`（僅面單訂單）：承運商運單號和 base64 編碼的面單 PDF。
- `result` `false` 且代碼為 `LABEL_PURCHASE_FAILED`（僅面單訂單）：訂單已存在，但沒有面單。請保留 `id` 並繼續執行第 11 步。

**GraphQL：** `uniorderCreate`（[GraphQL 手冊](/api/graphql/documentation#/orders/uniorderCreate)）

```graphql
mutation CreateBouquetOrder($packages: [Json]!) {
  uniorderCreate(
    rate_id: "eyJpdiI6Ik1rT2Z..."
    ref: "WEB-10045"
    type: "D"
    from_name: "Fleurs du Plateau"
    from_telephone: "5145550100"
    from_address: "4500 Rue Saint-Denis"
    from_city: "Montreal"
    from_province: "QC"
    from_country: "CA"
    from_postcode: "H2J2L3"
    to_name: "Jane Recipient"
    to_telephone: "5145550199"
    to_address: "6841 Rue Saint-Denis"
    to_city: "Montreal"
    to_province: "QC"
    to_country: "CA"
    to_postcode: "H2S2S3"
    packages: $packages
  )
}
```

變數：

```json
{ "packages": [{ "ref": "WEB-10045-1", "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }] }
```

**驗證：** `result` 為 `true` 且 `id` 已設定。已過期或屬於其他帳號的 `rate_id` 回傳 `400` 及代碼 `RATE_ID_INVALID`，且不會建立任何內容。

## 7. 列印面單

訂單一經建立，包裝工作站即列印面單。同一呼叫對配送訂單回傳商戶自有面單，對面單訂單回傳已購買的承運商面單。

**REST：** `GET /api/v1/uniorder/{orderId}/label` — [REST 手冊](/api/documentation#/paths/v1-uniorder-orderId--label/get)

```bash
curl "https://YOUR_HOST/api/v1/uniorder/123456/label?hide_sender_address=0" \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "format": "pdf",
  "tracking_numbers": ["SR26092800001"],
  "pdf_data": "JVBERi0xLjQK..."
}
```

- `pdf_data`：base64 編碼的面單 PDF。解碼後將檔案傳送到印表機。
- `hide_sender_address`、`hide_receiver_address`（`1` 表示隱藏）：適用於配送訂單的商戶自有面單。
- `label_status`（面單訂單）：回傳檔案時為 `ready`。承運商尚未產生檔案時，回應為 `200`，帶有 `result` `false` 和 `label_status` `pending`；請稍後再次請求面單。
- 本呼叫不會購買面單：尚未購買的面單回傳 `409` 及代碼 `LABEL_PURCHASE_FAILED`。請透過第 11 步購買。

**GraphQL：** `uniorderLabel`（[GraphQL 手冊](/api/graphql/documentation#/orders/uniorderLabel)）

```graphql
query {
  uniorderLabel(order_id: 123456, hide_sender_address: 0)
}
```

**驗證：** `result` 為 `true`，解碼後的 `pdf_data` 可作為 PDF 開啟，並顯示該訂單的運單號。

## 8. 查詢訂單

網路商店查詢訂單，以便在訂單頁面或客服介面上顯示其狀態、地址和包裹。

**REST：** `GET /api/v1/uniorder/{orderId}` — [REST 手冊](/api/documentation#/paths/v1-uniorder-orderId/get)

```bash
curl https://YOUR_HOST/api/v1/uniorder/123456 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "ref": "WEB-10045",
  "order_type": "D",
  "status": "pending",
  "created_at": "2026-10-02 09:14:05",
  "time_window_start": "2026-10-02 13:00:00",
  "time_window_end": "2026-10-02 17:00:00",
  "from": { "name": "Fleurs du Plateau", "address": "4500 Rue Saint-Denis", "city": "Montreal", "postcode": "H2J2L3" },
  "to": { "name": "Jane Recipient", "address": "6841 Rue Saint-Denis", "address_2": "Apt 2", "city": "Montreal", "postcode": "H2S2S3" },
  "packages": [
    { "id": 998877, "ref": "WEB-10045-1", "tracking_number": "SR26092800001", "weight": 1.2 }
  ],
  "shipping_price": "14.60",
  "currency": "CAD"
}
```

- `type`：`self_delivery` 或 `label_service`；其他欄位對兩者採用相同的結構。
- `status`：配送訂單為 `pending`、`in_transit`、`out_for_pickup`、`out_for_delivery`、`ready_for_self_pickup`、`delivered`、`exception` 或 `cancelled`，面單訂單為 `label_pending`、`label_purchased` 或 `cancelled`。
- `label`（僅面單訂單）：承運商、服務、`carrier_tracking_numbers` 和 `label_status`（`not_purchased`、`pending`、`ready` 或 `failed`）。

**GraphQL：** `uniorder`（[GraphQL 手冊](/api/graphql/documentation#/orders/uniorder)）

```graphql
query {
  uniorder(order_id: 123456)
}
```

**驗證：** 訂單回傳其 `status` 和 `packages`，且 `ref` 與網路商店訂單一致。

## 9. 追蹤訂單

訂單頁面顯示貨件的時間軸。可在客戶開啟頁面時讀取，或透過 Webhook 保持最新。

**REST：** `GET /api/v1/uniorder/{orderId}/tracking` — [REST 手冊](/api/documentation#/paths/v1-uniorder-orderId--tracking/get)

```bash
curl https://YOUR_HOST/api/v1/uniorder/123456/tracking \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "status": "delivered",
  "tracking_numbers": ["SR26092800001"],
  "events": [
    { "code": "delivered", "description": "Delivered", "location": "Montreal", "time": "2026-10-02 15:42:10", "time_zone": "America/Toronto", "source": "shipper" }
  ],
  "proofs": [
    { "type": "photo", "url": "https://YOUR_HOST/storage/pod/123456.jpg", "uploaded_at": "2026-10-02 15:42:08" }
  ]
}
```

- `events`：時間軸，最新的在前，每條帶有 `code`、`description`、`location` 和時間。
- `proofs`：簽收憑證檔案。`status` 為 `delivered` 後顯示。
- `carrier`（僅面單訂單）：承運商名稱、運單號和追蹤連結（`tracking_url`）。

**GraphQL：** `uniorderTracking`（[GraphQL 手冊](/api/graphql/documentation#/orders/uniorderTracking)）

```graphql
query {
  uniorderTracking(order_id: 123456)
}
```

**驗證：** 追蹤呼叫回傳 `result` `true`、訂單 `status` 及其 `events`。

## 10. 取消訂單

客戶取消網路商店訂單時，網路商店使用同一呼叫取消配送訂單或面單訂單的貨件。面單會先在其承運商處作廢。

**REST：** `POST /api/v1/uniorder/{orderId}/cancel` — [REST 手冊](/api/documentation#/paths/v1-uniorder-orderId--cancel/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/123456/cancel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cancel-10045" \
  -d '{}'
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "already_cancelled": false,
  "message": "The order has been cancelled."
}
```

- `already_cancelled`：訂單在本次呼叫之前已被取消時為 `true`。視為成功處理。
- 訂單未被取消時，回應為 `409`，訂單保持不變：`ORDER_STATUS_NOT_CANCELLABLE`（為時已晚，無法取消）、`ORDER_CANCEL_REFUSED`（目前無法取消）或 `LABEL_CANCEL_FAILED`（承運商未作廢面單）。請保持網路商店訂單為未結狀態，並人工處理該貨件。

**GraphQL：** `uniorderCancel`（[GraphQL 手冊](/api/graphql/documentation#/orders/uniorderCancel)）

```graphql
mutation {
  uniorderCancel(order_id: 123456)
}
```

**驗證：** `result` 為 `true`。再次取消同一訂單回傳 `already_cancelled` `true`。

## 11. 稍後購買面單（僅在 LABEL_PURCHASE_FAILED 之後）

本步驟僅適用於建立時回應為 `LABEL_PURCHASE_FAILED` 的面單訂單。該回應為 `200`，帶有 `result` `false`、代碼 `LABEL_PURCHASE_FAILED` 和訂單 `id`：訂單已保留，但沒有面單。請勿再次提交訂單；請為該訂單購買面單。

**REST：** `POST /api/v1/uniorder/{orderId}/label` — [REST 手冊](/api/documentation#/paths/v1-uniorder-orderId--label/post)

未能購買面單的建立請求回傳的回應：

```json
{
  "result": false,
  "code": "LABEL_PURCHASE_FAILED",
  "type": "label_service",
  "id": 123458,
  "ref": "WEB-10046",
  "tracking_numbers": ["SR26092800003"],
  "quoted_price": "18.40",
  "message": "The quoted service is not offered for this shipment."
}
```

為訂單 `123458` 購買面單：

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/123458/label \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: label-10046" \
  -d '{}'
```

面單依建立訂單時所選的服務購買。如需依同一帳號的其他服務購買，請在請求本文中傳送第 5 步的新 `label_service` `rate_id`（`{"rate_id": "eyJpdiI6IlpxR0..."}`）。已購買的面單會直接回傳，不會再次購買。

```json
{
  "result": true,
  "type": "label_service",
  "id": 123458,
  "ref": "WEB-10046",
  "shipping_price": "18.40",
  "label": {
    "carrier_name": "ups",
    "service_code": "ups_standard",
    "main_tracking_number": "1Z999AA10123456791",
    "label_status": "ready",
    "shipping_label": "JVBERi0xLjQK..."
  }
}
```

- `label.shipping_label`：base64 編碼的面單 PDF；依第 7 步列印。
- 再次回傳 `result` `false` 及 `LABEL_PURCHASE_FAILED`：承運商仍然拒絕。請稍後重試，或使用新的 `rate_id` 依其他服務購買。

**GraphQL：** `uniorderPurchaseLabel`（[GraphQL 手冊](/api/graphql/documentation#/orders/uniorderPurchaseLabel)）

```graphql
mutation {
  uniorderPurchaseLabel(order_id: 123458)
}
```

**驗證：** `result` 為 `true` 且 `label.shipping_label` 包含 PDF，或在承運商產生檔案期間 `label.label_status` 為 `pending`。

## 12. 批次處理

批次呼叫可在一次呼叫中對多個貨件詢價或建立，例如 ERP 的批發訂單。每一行都依單個呼叫處理，並回傳單個呼叫會回傳的內容；某一行失敗不會影響其他行。

**REST：** `POST /api/v1/uniorder/rate/batch` — [REST 手冊](/api/documentation#/paths/v1-uniorder-rate-batch/post) · `POST /api/v1/uniorder/batch` — [REST 手冊](/api/documentation#/paths/v1-uniorder-batch/post)

每次呼叫最多 20 行，在同一回應中回傳：批次詢價使用 `shipments`，批次建立使用 `orders`。每一行的欄位與單個呼叫相同，另可附加一個可選的 `reference`，隨其結果一併回傳。

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/batch \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: erp-2026-10-01" \
  -d '{
    "orders": [
      {
        "reference": "ERP-7781",
        "rate_id": "eyJpdiI6Ik1rT2Z...",
        "ref": "ERP-7781",
        "from_name": "Fleurs du Plateau",
        "from_telephone": "5145550100",
        "from_address": "4500 Rue Saint-Denis",
        "from_city": "Montreal",
        "from_province": "QC",
        "from_country": "CA",
        "from_postcode": "H2J2L3",
        "to_name": "Jane Recipient",
        "to_telephone": "5145550199",
        "to_address": "6841 Rue Saint-Denis",
        "to_city": "Montreal",
        "to_province": "QC",
        "to_country": "CA",
        "to_postcode": "H2S2S3",
        "packages": [{ "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }]
      }
    ]
  }'
```

```json
{
  "result": true,
  "count": 1,
  "results": [
    { "index": 0, "reference": "ERP-7781", "status": 200, "body": { "result": true, "type": "self_delivery", "id": 123460 } }
  ]
}
```

- `results`：每行一筆，帶有該行的 `index`、其 `reference`，以及單個呼叫會回傳的 `status` 和 `body`。依 `reference` 將每筆結果與其訂單行對應。

**REST：** `POST /api/v1/uniorder/rate/batch-async` — [REST 手冊](/api/documentation#/paths/v1-uniorder-rate-batch-async/post) · `POST /api/v1/uniorder/batch-async` — [REST 手冊](/api/documentation#/paths/v1-uniorder-batch-async/post) · `GET /api/v1/uniorder/jobs/{jobId}` — [REST 手冊](/api/documentation#/paths/v1-uniorder-jobs-jobId/get)

最多 500 行，作為一個任務排隊處理。呼叫回傳 `job_id`；輪詢該任務直到 `status` 為 `done`，然後讀取 `results`。在第一個任務仍在排隊時再次傳送相同的批次請求，將回傳第一個任務並帶有 `duplicate` `true`。

```bash
curl https://YOUR_HOST/api/v1/uniorder/jobs/8813 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "job_id": 8813,
  "kind": "create",
  "status": "done",
  "count": 1,
  "results": [
    { "index": 0, "reference": "ERP-7781", "status": 200, "body": { "result": true, "type": "self_delivery", "id": 123460 } }
  ]
}
```

- `status`：`queued`、`done`，或在任務無法處理時為 `failed` 並附帶 `message`。
- 任務只執行一次，不會重試。在某行執行前過期的 `rate_id` 會使該行回傳 `RATE_ID_INVALID`；請在詢價任務完成後盡快傳送建立任務。

**GraphQL：** `uniorderRateBatch`（[GraphQL 手冊](/api/graphql/documentation#/orders/uniorderRateBatch)） · `uniorderCreateBatch`（[GraphQL 手冊](/api/graphql/documentation#/orders/uniorderCreateBatch)） · `uniorderRateBatchAsync`（[GraphQL 手冊](/api/graphql/documentation#/orders/uniorderRateBatchAsync)） · `uniorderCreateBatchAsync`（[GraphQL 手冊](/api/graphql/documentation#/orders/uniorderCreateBatchAsync)） · `uniorderJob`（[GraphQL 手冊](/api/graphql/documentation#/orders/uniorderJob)）

```graphql
query {
  uniorderJob(job_id: 8813)
}
```

**驗證：** 批次請求每行回傳一筆結果；非同步任務最終達到 `status` `done`。

## 13. 錯誤處理

| 情形 | HTTP 狀態 | 代碼 | 整合方的處理 |
|---|---|---|---|
| 必填欄位缺失或格式錯誤 | 400 | `VALIDATION_FAILED` | 更正 `message` 中指出的欄位後重新傳送請求。 |
| 收件人位於配送區域外（詢價） | 200 | `errors` 中的 `OUT_OF_DELIVERY_AREA` | 僅提供 `label_service` 報價。 |
| `rate_id` 已過期、格式錯誤或屬於其他帳號 | 400 | `RATE_ID_INVALID` | 重新詢價，並使用新的 `rate_id` 建立。未建立任何內容。 |
| 面單訂單已建立，但面單未購買 | 200（`result` `false`） | `LABEL_PURCHASE_FAILED` | 保留 `id`；透過 `POST /api/v1/uniorder/{orderId}/label` 購買面單。切勿重新建立訂單。 |
| 在面單購買之前請求面單 | 409 | `LABEL_PURCHASE_FAILED` | 透過 `POST /api/v1/uniorder/{orderId}/label` 購買面單。 |
| 訂單進度已無法取消 | 409 | `ORDER_STATUS_NOT_CANCELLABLE` | 保持訂單現狀；另行處理退貨。 |
| 訂單目前無法取消 | 409 | `ORDER_CANCEL_REFUSED` | 保持訂單現狀；稍後重試或聯絡商戶。 |
| 承運商未作廢面單 | 409 | `LABEL_CANCEL_FAILED` | 訂單未變更；稍後重試取消。 |
| 訂單或任務不存在，或屬於其他帳號 | 404 | `ORDER_NOT_FOUND` | 檢查與網路商店訂單一起保存的 `id`。 |
| `Idempotency-Key` 被用於不同的請求本文 | 409 | `IDEMPOTENCY_CONFLICT` | 不同的請求使用新的鍵。 |
| 權杖缺失或已過期，或帳號無下單權限 | 401 | — | 重新登入；檢查帳號權限。 |

## 驗收清單

請使用 `WEB-10045` 等測試 `ref`：

- [ ] 對區域內地址，詢價回傳一條 `self_delivery` 報價。
- [ ] 使用 `quote_labels` 時，詢價回傳 `label_service` 報價，每條帶有 `rate_id`。
- [ ] 使用 `self_delivery` 的 `rate_id` 下單，回傳 `id` 和 `tracking_numbers`。
- [ ] 使用 `label_service` 的 `rate_id` 下單，回傳所詢服務的面單。
- [ ] 相同的 `Idempotency-Key` 不會建立第二張訂單。
- [ ] 超過 30 分鐘的 `rate_id` 回傳 `RATE_ID_INVALID`。
- [ ] 每張訂單的面單解碼後均為可列印的 PDF。
- [ ] 可使用建立時回傳的 `id` 查詢訂單、其面單和追蹤資訊。
- [ ] 取消測試訂單回傳 `result: true`；再次取消回傳 `already_cancelled: true`。
- [ ] 出現 `LABEL_PURCHASE_FAILED` 後，`POST /api/v1/uniorder/{orderId}/label` 為同一訂單購買面單。
- [ ] 兩行的批次請求回傳兩筆帶有各自 `reference` 的結果。
