# 承運商打單

向承運商下單：列出運輸方式 → 詢價 → 建立面單 → 下載 PDF → 追蹤 → 接收事件通知 → 取消（或日結）。

本專題僅涵蓋下列介面，請按順序呼叫。

請將 `YOUR_HOST`、`ACCESS_TOKEN` 與 `shipping_method` 替換為實際環境中的值。運輸方式 `id` 因帳號而異，請勿硬編碼。

## 1. 登入

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

```
Authorization: Bearer ACCESS_TOKEN
```

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

[REST 手冊](/api/documentation#/paths/v1-user-login/post) · [GraphQL 手冊](/api/graphql/documentation#/user/userLogin)

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

## 2. 列出運輸方式

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/getShippingMethodList \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"detail": true}'
```

回傳列表中的每一項包含：

| 欄位 | 用途 |
|---|---|
| `id` | 後續請求中的 `shipping_method` 參數 |
| `name` | 顯示名 |
| `unique_identifier` | 穩定編碼 |
| `options.signature_option` | 是否支援簽名 |
| `options.insurance_option` | 是否支援保險 |
| `options.multi_package` | 是否支援多件 |

**GraphQL：** `labelserviceGetShippingMethodList` ([GraphQL 手冊](/api/graphql/documentation#/labelservice/labelserviceGetShippingMethodList))（JSON 標量）。

**驗證：** 列表非空。你選出一個 `id`，並知道該方式是否支援簽名、保險、多件。空列表說明帳號上還沒啟用任何方式。

## 3. 詢價

試算。會問承運商要價格，但不會訂艙。報文形狀與建立相同。`shipping_method` 必填。

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/rate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "shipping_method": 59,
    "name": "Jane Recipient",
    "telephone": "5555555555",
    "email": "jane@example.com",
    "address_1": "6701 RUE HADLEY",
    "city": "Montreal",
    "province": "QC",
    "postcode": "H4E3R3",
    "country": "CA",
    "weight": 1,
    "length": 30,
    "width": 20,
    "height": 10,
    "dimension_unit": 2,
    "weight_unit": 2,
    "signature_option": 0,
    "insurance_option": 0,
    "package_type": "parcel",
    "ref": "DEV-LABEL-001",
    "sender_name": "Acme Warehouse",
    "sender_telephone": "5555555555",
    "sender_address_1": "2667 boul des sources",
    "sender_city": "Pointe-Claire",
    "sender_province": "QC",
    "sender_postcode": "H9S2H9",
    "sender_country": "CA"
  }'
```

`weight_unit`：`1` 克，`2` 千克，`3` 盎司，`4` 磅。`dimension_unit`：`1` 毫米，`2` 厘米，`3` 米，`4` 英寸。

多件用 `packages: [{ ref, weight, length, width, height, weight_unit, dimension_unit }]`。`shipping_from`（地址簿 id）或 `shipping_from_code` 可以代替 `sender_*`。

**GraphQL：** `labelserviceRate` ([GraphQL 手冊](/api/graphql/documentation#/labelservice/labelserviceRate))。

**驗證：** `result` 為 true，並且有價格（承運商給了時效時也會帶回）。沒有價格就先改目的地 / 包裹 / 方式，**不要建立**。

## 4. 建立面單

此時會向承運商訂艙。

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

請求體與第 3 步相同，並須包含 `Idempotency-Key`。

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/submitOrder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dev-label-001" \
  -d '{
    "shipping_method": 59,
    "name": "Jane Recipient",
    "telephone": "5555555555",
    "email": "jane@example.com",
    "address_1": "6701 RUE HADLEY",
    "city": "Montreal",
    "province": "QC",
    "postcode": "H4E3R3",
    "country": "CA",
    "weight": 1,
    "length": 30,
    "width": 20,
    "height": 10,
    "dimension_unit": 2,
    "weight_unit": 2,
    "signature_option": 0,
    "insurance_option": 0,
    "package_type": "parcel",
    "ref": "DEV-LABEL-001",
    "sender_name": "Acme Warehouse",
    "sender_telephone": "5555555555",
    "sender_address_1": "2667 boul des sources",
    "sender_city": "Pointe-Claire",
    "sender_province": "QC",
    "sender_postcode": "H9S2H9",
    "sender_country": "CA"
  }'
```

**GraphQL：** `labelserviceSubmitOrder` ([GraphQL 手冊](/api/graphql/documentation#/labelservice/labelserviceSubmitOrder))。

```json
{
  "result": true,
  "id": 12345,
  "shipping_price": "12.50",
  "tracking_numbers": ["1Z999AA10123456784"],
  "external_id": "EXT_12345"
}
```

| 欄位 | 用途 |
|---|---|
| `id` | Superroute 訂單 id——下載和取消 |
| `tracking_numbers` | 提供給收件人；公開追蹤介面可查詢這些運單號 |
| `external_id` | 承運商貨件 id |
| `shipping_price` | 實扣金額 |

**驗證：** `tracking_numbers` 非空。保存 `id` 和運單號。同一 `Idempotency-Key` 不得再買第二張面單。

## 5. 下載 PDF

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/getShippingLabel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "12345",
    "type": "ORDER_ID",
    "base64": 1
  }'
```

`type` 可以是 `ORDER_ID`、`TRACKING_NUMBER` 或 `REF`。`base64: 0` 下發 PDF 流。這是**承運商官方面單**。件數在訂艙時已固定。

**GraphQL：** `labelserviceGetShippingLabel` ([GraphQL 手冊](/api/graphql/documentation#/labelservice/labelserviceGetShippingLabel))。

**驗證：** PDF 可正常開啟，並顯示第 4 步的承運商條碼 / 運單號。測試面單僅供驗證，請勿交付承運商。

## 6. 追蹤

無需權杖。使用 `tracking_numbers` 裡的號：

```bash
curl https://YOUR_HOST/api/v1/tracking/1Z999AA10123456784
```

[REST 手冊](/api/documentation#/operations/getPublicTracking) · [GraphQL 手冊](/api/graphql/documentation#/tracking/trackingPublic)

**GraphQL：**

```graphql
query {
  trackingPublic(trackingNumber: "1Z999AA10123456784") {
    result
    deliveried
    is_third_party_tracking
    data {
      tracking_event_status_id
      tracking_event_key
      description
      updated_at_localized
    }
    proofs { file_id type full_url signed_url }
  }
}
```

事件來自承運商時 `is_third_party_tracking` 為 true。按 `tracking_event_status_id` / `tracking_event_key` 分支，不要按 `description`。`data` 最新在前。承運商掃描前，早期事件可能仍是「資訊已提交」。`500` 是已送達；此時 `proofs[]` 可能有簽名（`type` `1`）或照片（`type` `2`）。

**驗證：** 能查到剛建立的那票。未知運單號回傳 `result: false` 或 HTTP 404。

## 7. 設定事件通知

| 設定項 | 事件 | 觸發時機 |
|---|---|---|
| `order_create_webhook_url` | `order.created` | 保存 `id` 和承運商運單號 |
| `tracking_event_webhook_url` | `tracking.event` | 掃描、派送中、已送達 |
| `order_status_change_webhook_url` | `order.status_change` | 業務系統中的訂單狀態 |
| `order_cancel_failed_webhook_url` | `order.cancel_failed` | 因承運商已拿到貨而取消失敗 |

**REST:** `PUT /api/v1/webhook-settings` — [REST 手冊](/api/documentation#/paths/v1-webhook-settings/put)

**GraphQL:** `webhookSettingsUpdate` ([GraphQL 手冊](/api/graphql/documentation#/webhooks/webhookSettingsUpdate)).

```bash
curl -X PUT https://YOUR_HOST/api/v1/webhook-settings \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "order_create_webhook_url": "https://erp.example.com/hooks/superroute",
    "tracking_event_webhook_url": "https://erp.example.com/hooks/superroute",
    "webhook_sign_secret": "YOUR_LONG_RANDOM_SECRET",
    "webhook_verify_ssl": 1
  }'
```

用**原始報文**校驗 **v2**：`HMAC_SHA256(timestamp + "." + raw_body, secret)` 對照 `X-Webhook-Signature-V2`。用 `X-Webhook-Event-Id` 去重。**3 秒內回傳 2xx**。

```php
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE_V2'] ?? '';
$expected = hash_hmac('sha256', $ts . '.' . $raw, $sharedSecret);
if (!hash_equals($expected, $sig)) {
    http_response_code(401);
    exit;
}
```

**驗證：** 一次測試 `submitOrder` 能產生帶那些 `tracking_numbers` 的 `order.created`。簽名校驗失敗時，接收端應回傳 `401`。

## 8. 取消

僅在承運商仍允許時（通常是攬收前）。

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/cancelShippingLabel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":"12345","type":"ORDER_ID"}'
```

**GraphQL：** `labelserviceCancelShippingLabel` ([GraphQL 手冊](/api/graphql/documentation#/labelservice/labelserviceCancelShippingLabel))。

承運商已經拿到貨會拒絕——對應 `order.cancel_failed`。

**驗證：** 第二次取消是安全的。公開追蹤不再將該運單視為有效在途件。

## 9. 日結（僅當該方式要求時）

有的承運商需要每日艙單。

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/endofday \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"shipping_method": 59}'
```

**GraphQL：** `labelserviceEndofday` ([GraphQL 手冊](/api/graphql/documentation#/labelservice/labelserviceEndofday))。

當天最後一張面單之後呼叫一次。第 2 步的方式沒有這項要求時跳過。

**驗證：** 成功體（或承運商確認）列出當天的面單。先在測試方式上跑通。

## 驗收清單

用你能控制的目的地，以及可以取消的方式：

- [ ] 方式列表非空；已記錄一個 `id`。
- [ ] 該方式 + 目的地詢價能回傳價格。
- [ ] 提交回傳 `tracking_numbers`；同一 `Idempotency-Key` 不會買第二張面單。
- [ ] 面單 PDF 可正常開啟並顯示承運商運單號。
- [ ] 公開追蹤能用該號查到。
- [ ] 應收到 `order.created`，且 v2 簽名校驗通過。
- [ ] 取消成功，**或**你已確認該方式訂艙後不能取消。
- [ ] 若該方式需要日結，測試跑通無報錯。
