# 承运商打单

面单服务从账号所连接的承运商（例如 UPS 和 Canada Post）购买运输面单，并将每张面单作为一张订单保存。集成方列出账号的运输方式、对包裹询价、创建面单订单、按所选承运商服务购买面单、打印 PDF、追踪包裹，并取消未使用的面单。本专题面向通过承运商而非自有司机寄送包裹的网店、仓储系统和订单管理系统。

## 1. 可以构建的应用

本专题中的示例均以同一家商户为例：**Northbound Outfitters**，一家户外用品网店，从位于 1200 Eglinton Ave E, Toronto 的仓库发货。其账号有一个 Canada Post 运输方式和一个 UPS 运输方式。一张典型订单是一个 4.2 kg、60 × 30 × 25 cm 的帐篷箱，寄往 Calgary；寄往美国的订单使用 UPS。

- **结账时选择承运商。** 网店针对 Canada Post 为客户的购物车询价，显示各项服务的价格和运输天数，并按客户付费的服务发货。
- **仓库一键打印面单。** 包装工位在箱子打包完成时创建面单订单，按所选服务购买面单，并在热敏打印机上打印承运商 PDF。
- **附带报关数据的跨境货件。** 寄往美国的订单带有货物明细行（描述、数量、价值、HS 编码），使 UPS 面单附带商业数据出具。
- **自动向客户更新状态。** 网店保存承运商运单号，在订单页面上显示公开追踪时间线，并在 `tracking.event` Webhook 报告包裹已送达时更新订单。

## 2. 本专题的适用范围

本专题介绍 v1 面单服务（`/api/v1/labelservice/...`）：每次调用对应一个运输方式（一个承运商账号）。当集成方已确定使用哪种运输方式发货，或需要维护现有的面单服务集成时，请使用本专题。

对于新集成，推荐使用 Uniorder（`/api/v1/uniorder/...`）作为统一入口。Uniorder 专题“Uniorder：所有货件统一一个接口”和“一次询价与下单”一次性对账号的所有承运商服务询价（适用时同时包含商户自有配送），并通过回传所选服务的 `rate_id` 购买该服务的面单。之后使用相同的调用对每张订单进行打印、追踪和取消。

其他专题涵盖其他货件类型：

- 由商户自有司机配送：“自有车队取件与配送”。
- 客户账号通过其所属商户提供的服务发货：“运输服务”。
- 货物存放在仓库中并按请求出库：“仓储与出库”。

## 3. 开始之前

- **账号。** 使用商户（客户）账号、该账号的员工，或某商户下的客户账号。商户账号可以看到自己的运输方式。客户账号只能看到其商户分配给它的运输方式，所购买的每张面单均从其余额中扣费；若商户为该客户启用了“自动暂停面单服务”，当余额加信用额度不足以支付面单时，面单将被拒绝。
- **API 权限。** 账号必须已开通 API 访问。未开通时，每个面单服务调用都返回 `401` 及 `Unauthorized`。
- **运输方式。** 账号上必须至少有一个有效的运输方式（对客户而言：已分配给该客户）。运输方式标识因账号而异，不得硬编码；请在第 5 步读取。
- **测试数据。** 在已配置的情况下，使用测试运输方式或承运商沙箱（此时报价带有 `test_mode: true`），以及您可控制的目的地。在正式运输方式上购买的每张测试面单都应取消。
- **令牌处理。** 从您的服务器登录，并将令牌保存在服务器上。不要将令牌或密码放在浏览器或移动应用中。
- **占位符。** 将 `YOUR_HOST` 替换为您的平台主机，将 `ACCESS_TOKEN` 替换为第 4 步获得的令牌。将 `shipping_method` 的值替换为您账号的 id。

## 4. 登录

每个面单服务调用都需要 Bearer 令牌。集成方登录一次，在服务器上保存 `access_token` 和 `expires_at`，并在令牌过期前重新登录。

**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":"shipping@northbound-outfitters.ca","password":"your_password"}'
```

```json
{
  "result": true,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOi...",
  "token_type": "Bearer",
  "expires_at": "2027-09-28 10:15:00"
}
```

- `access_token`：在之后的每个调用中按下方请求头发送。
- `expires_at`：在此时间之前重新登录。

```
Authorization: Bearer ACCESS_TOKEN
```

GraphQL 请求提交至 `POST /api/graphql`，并使用相同的请求头。

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

**验证：** 登录成功后返回 `access_token`。后续请求若未携带该令牌，将返回 `401`。

## 5. 列出运输方式

运输方式列表告诉集成方可以使用哪些承运商账号发货，以及每个账号接受哪些选项。请保存您使用的每个运输方式的 `id`；它是之后每个调用中的 `shipping_method`。

**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}'
```

```json
[
  {
    "id": 59,
    "name": "Canada Post",
    "unique_identifier": "CPC-TOR",
    "options": {
      "signature_option": true,
      "insurance_option": true,
      "insurance_value": true,
      "multi_package": true
    },
    "package_type": {
      "parcel": {
        "name": "Parcel",
        "options": { "weight_options": true, "dimension_options": true }
      }
    },
    "from_contry_limit": ["CA"],
    "isUploadMethod": false
  },
  {
    "id": 61,
    "name": "UPS",
    "unique_identifier": "UPS-TOR",
    "options": {
      "signature_option": true,
      "insurance_option": true,
      "insurance_value": true,
      "multi_package": true
    }
  }
]
```

每一行包含：

| 字段 | 用途 |
|---|---|
| `id` | 之后每个调用中的 `shipping_method` |
| `name` | 显示名称 |
| `unique_identifier` | 固定代码 |
| `options.signature_option` | 是否提供签收 |
| `options.insurance_option` | 是否提供保险 |
| `options.multi_package` | 是否支持多件 |
| `package_type` | 接受的 `package_type` 代码，以及各代码是否需要重量和尺寸 |
| `from_contry_limit` | 寄件地址允许所在的国家 |
| `services` | 该运输方式背后的承运商和服务；可用这些代码通过 `carriers` / `services` 限定询价范围 |

发送 `"id": 59` 只读取一个运输方式，或发送 `"detail": false` 只接收 `id`、`name` 和 `unique_identifier`。

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

**验证：** 列表不为空。您已选定一个 `id`，并了解该运输方式是否支持签收、保险和多件包裹。列表为空表示账号上未启用任何运输方式。

## 6. 询价

询价向承运商获取价格，但不创建任何内容：请求所用的临时订单会被删除，也不会产生任何费用。Northbound Outfitters 在结账时调用它，为购物车显示 Canada Post 的各项服务。请求体结构与第 7 步相同。`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": "Emily Tremblay",
    "telephone": "4035550182",
    "email": "emily.tremblay@example.com",
    "address_1": "1415 17 Ave SW",
    "city": "Calgary",
    "province": "AB",
    "postcode": "T2T0C8",
    "country": "CA",
    "weight": 4.2,
    "length": 60,
    "width": 30,
    "height": 25,
    "dimension_unit": 2,
    "weight_unit": 2,
    "signature_option": 0,
    "insurance_option": 0,
    "package_type": "parcel",
    "ref": "NB-10482",
    "sender_name": "Northbound Outfitters",
    "sender_telephone": "4165550140",
    "sender_address_1": "1200 Eglinton Ave E",
    "sender_city": "Toronto",
    "sender_province": "ON",
    "sender_postcode": "M3C1H9",
    "sender_country": "CA"
  }'
```

```json
{
  "result": true,
  "shipping_price": "24.86",
  "price_details": { "shipping_fee": "24.86" },
  "rates": [
    {
      "carrier_name": "canadapost",
      "currency": "CAD",
      "service_code": "canadapost_expedited_parcel",
      "service_name": "CANADAPOST EXPEDITED PARCEL",
      "price": 24.86,
      "transit_days": 3,
      "test_mode": false
    },
    {
      "carrier_name": "canadapost",
      "currency": "CAD",
      "service_code": "canadapost_xpresspost",
      "service_name": "CANADAPOST XPRESSPOST",
      "price": 38.12,
      "transit_days": 2,
      "test_mode": false
    }
  ],
  "best_rate": {
    "service_code": "canadapost_expedited_parcel",
    "price": 24.86,
    "transit_days": 3
  }
}
```

- `rates[]`：每项承运商服务一条，带有 `price`、`currency`、`transit_days` 和 `price_detail`（基本运费、燃油附加费、税费）。请向客户显示这些信息。
- `best_rate` / `shipping_price`：该运输方式返回的第一条报价。
- 询价不带 `rate_id`，也不带订单 `id`。面单从第 7 步创建的订单的报价中购买。

`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_*` 字段组。`carriers` 和 `services` 将询价限定为所列代码。

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

**验证：** `result` 为 true，且您获得了价格（承运商提供时还包括运输天数）。若没有报价，请在创建**之前**修正目的地、包裹或运输方式。

## 7. 创建面单订单

本调用创建面单订单，并向承运商获取该货件的报价。它返回订单 `id`，以及每项服务一个 `rate_id`。此时面单尚未购买，也不产生费用；第 8 步购买面单。Northbound Outfitters 在箱子打包完成时调用它，并将 `id` 与其订单 `NB-10482` 关联保存。

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

请求体与第 6 步相同。请发送 `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: nb-10482-label" \
  -d '{
    "shipping_method": 59,
    "name": "Emily Tremblay",
    "telephone": "4035550182",
    "email": "emily.tremblay@example.com",
    "address_1": "1415 17 Ave SW",
    "city": "Calgary",
    "province": "AB",
    "postcode": "T2T0C8",
    "country": "CA",
    "weight": 4.2,
    "length": 60,
    "width": 30,
    "height": 25,
    "dimension_unit": 2,
    "weight_unit": 2,
    "signature_option": 0,
    "insurance_option": 0,
    "package_type": "parcel",
    "ref": "NB-10482",
    "sender_name": "Northbound Outfitters",
    "sender_telephone": "4165550140",
    "sender_address_1": "1200 Eglinton Ave E",
    "sender_city": "Toronto",
    "sender_province": "ON",
    "sender_postcode": "M3C1H9",
    "sender_country": "CA"
  }'
```

```json
{
  "result": true,
  "id": 128455,
  "shipping_price": "24.86",
  "price_details": { "shipping_fee": "24.86" },
  "rates": [
    {
      "rate_id": "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c10",
      "carrier_name": "canadapost",
      "service_code": "canadapost_expedited_parcel",
      "service_name": "CANADAPOST EXPEDITED PARCEL",
      "price": 24.86,
      "currency": "CAD",
      "transit_days": 3
    },
    {
      "rate_id": "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c11",
      "carrier_name": "canadapost",
      "service_code": "canadapost_xpresspost",
      "service_name": "CANADAPOST XPRESSPOST",
      "price": 38.12,
      "currency": "CAD",
      "transit_days": 2
    }
  ],
  "best_rate": {
    "rate_id": "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c10",
    "service_code": "canadapost_expedited_parcel",
    "price": 24.86
  }
}
```

| 字段 | 用途 |
|---|---|
| `id` | Superroute 订单 id —— 用于购买、下载和取消 |
| `rates[].rate_id` | 第 8 步要购买的服务；仅对本订单有效 |
| `rates[].price` | 该服务的价格 |
| `shipping_price` | `best_rate` 的价格 |

寄往美国的货件通过 UPS 运输方式发送，并带有报关所需的货物明细行。本示例使用 `packages` 形式，按箱携带货物明细：

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/submitOrder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: nb-10497-label" \
  -d '{
    "shipping_method": 61,
    "name": "Daniel Price",
    "telephone": "2065550117",
    "email": "daniel.price@example.com",
    "address_1": "500 Mercer St",
    "city": "Seattle",
    "province": "WA",
    "postcode": "98109",
    "country": "US",
    "package_type": "parcel",
    "paid_by": 1,
    "ref": "NB-10497",
    "sender_name": "Northbound Outfitters",
    "sender_telephone": "4165550140",
    "sender_address_1": "1200 Eglinton Ave E",
    "sender_city": "Toronto",
    "sender_province": "ON",
    "sender_postcode": "M3C1H9",
    "sender_country": "CA",
    "packages": [
      {
        "ref": "NB-10497-1",
        "weight": 2.6,
        "length": 45,
        "width": 30,
        "height": 20,
        "weight_unit": 2,
        "dimension_unit": 2,
        "items": [
          {
            "name": "Down sleeping bag",
            "description": "Down-filled sleeping bag, -7 C rating",
            "quantity": 1,
            "unit_price": 289.00,
            "currency": "CAD",
            "weight": 1.6,
            "hscode": "9404400000",
            "sku": "NB-SB-7C",
            "unit": "PCS"
          },
          {
            "name": "Camp stove",
            "description": "Canister camp stove",
            "quantity": 1,
            "unit_price": 79.00,
            "currency": "CAD",
            "weight": 1.0,
            "hscode": "7321111000",
            "sku": "NB-ST-01",
            "unit": "PCS"
          }
        ]
      }
    ]
  }'
```

**GraphQL：** `labelserviceSubmitOrder`（[GraphQL 手册](/api/graphql/documentation#/labelservice/labelserviceSubmitOrder)）。REST 请求体放在 `input` 中：

```graphql
mutation {
  labelserviceSubmitOrder(input: {
    shipping_method: 59
    name: "Emily Tremblay"
    telephone: "4035550182"
    address_1: "1415 17 Ave SW"
    city: "Calgary"
    province: "AB"
    postcode: "T2T0C8"
    country: "CA"
    weight: 4.2
    length: 60
    width: 30
    height: 25
    dimension_unit: 2
    weight_unit: 2
    package_type: "parcel"
    ref: "NB-10482"
    sender_name: "Northbound Outfitters"
    sender_telephone: "4165550140"
    sender_address_1: "1200 Eglinton Ave E"
    sender_city: "Toronto"
    sender_province: "ON"
    sender_postcode: "M3C1H9"
    sender_country: "CA"
  })
}
```

**验证：** 响应包含 `id` 以及至少一个 `rates[].rate_id`。请保存两者。使用相同的 `Idempotency-Key` 和相同的请求体返回相同的 `id`，且不会创建第二张订单。

## 8. 购买面单并读取货件详情

本调用按所选服务购买面单、扣费，并返回承运商运单号。面单已购买时，只读取详情，因此重复调用不会重复购买。Northbound Outfitters 发送客户付费的服务的 `rate_id`。

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

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

```json
{
  "id": 128455,
  "shippingPrice": "24.86",
  "mainTrackingNumber": "7023210039414604",
  "trackingNumber": "7023210039414604",
  "needSubmitShippingInformation": false,
  "rate": {
    "carrier_name": "canadapost",
    "price": 24.86,
    "price_detail": [
      { "name": "Base charge", "amount": 18.40 },
      { "name": "Fuel surcharge", "amount": 3.60 },
      { "name": "GST", "amount": 1.10 }
    ],
    "tax_items": ["HST", "GST", "PST", "QST"]
  },
  "labelStatus": "ready",
  "shippingLabel": "JVBERi0xLjQKMS... (base64 encoded)"
}
```

| 字段 | 用途 |
|---|---|
| `mainTrackingNumber` | 第一个包裹的承运商运单号；提供给客户 |
| `trackingNumber` | 所有包裹的承运商运单号，以逗号分隔 |
| `shippingPrice` | 扣费金额 |
| `labelStatus` | `ready`：`shippingLabel` 中包含 PDF。`pending`：已购买并扣费，但承运商尚未生成文件；请稍后再次调用。`failed`：后台获取已放弃；再次调用会重新开始获取 |
| `needSubmitShippingInformation` | 该运输方式需要提交货件信息时为 `true`（第 13 步） |

`type` 可以是 `ORDER_ID`（默认）、`TRACKING_NUMBER`（Superroute 包裹单号）或 `THIRD_PARTY_TRACKING_NUMBER`（承运商单号）。请发送 `rate_id`，使面单按您所选的服务购买；不发送时，该运输方式按其默认报价购买。

**GraphQL：** `labelserviceGetShippingDetail`（[GraphQL 手册](/api/graphql/documentation#/labelservice/labelserviceGetShippingDetail)）

```graphql
mutation {
  labelserviceGetShippingDetail(
    id: "128455"
    type: "ORDER_ID"
    rate_id: "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c10"
  )
}
```

**验证：** `mainTrackingNumber` 不为空，且 `labelStatus` 为 `ready`（或为 `pending`，之后的调用中变为 `ready`）。第二次调用返回相同的运单号和相同的 `shippingPrice`。

## 9. 下载 PDF

仓库通过本调用打印承运商面单。若面单尚未购买，第一次调用会按默认报价购买，与第 8 步相同；如需确定服务，请先调用第 8 步。

**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": "128455",
    "type": "ORDER_ID",
    "base64": 1
  }'
```

```json
"JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURlY29kZT4+..."
```

- 使用 `base64: 1` 时，响应体为一个 base64 字符串形式的 PDF；解码后发送到打印机。
- 使用 `base64: 0` 时，响应即为 PDF 文件本身（`application/pdf`）。

`type` 可以是 `ORDER_ID`（默认）、`TRACKING_NUMBER` 或 `THIRD_PARTY_TRACKING_NUMBER`（承运商单号）。这是**承运商官方面单**。件数由下单时确定。

**GraphQL：** `labelserviceGetShippingLabel`（[GraphQL 手册](/api/graphql/documentation#/labelservice/labelserviceGetShippingLabel)）。GraphQL 始终返回 base64 字符串。

**验证：** PDF 可以打开，并显示第 8 步的承运商条码 / 运单号。打印一份测试副本后将其丢弃——不要将测试面单交给承运商。

## 10. 追踪

网店在客户的订单页面上显示包裹的运输进度。公开追踪接口不需要令牌，并接受第 8 步的承运商单号。

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

```bash
curl https://YOUR_HOST/api/v1/tracking/7023210039414604
```

```json
{
  "result": true,
  "is_third_party_tracking": true,
  "deliveried": false,
  "data": [
    {
      "tracking_event_status_id": 430,
      "otep_status": "in_transit",
      "description": "Item in transit",
      "location_city": "Mississauga",
      "updated_at_localized": "2026-09-29 18:42"
    }
  ]
}
```

**GraphQL：** `trackingPublic`（[GraphQL 手册](/api/graphql/documentation#/tracking/trackingPublic)）

```graphql
query {
  trackingPublic(trackingNumber: "7023210039414604") {
    result
    deliveried
    is_third_party_tracking
    data {
      tracking_event_status_id
      otep_status
      description
      updated_at_localized
    }
    third_party_info { tracking_number status carrier_tracking_link }
    proofs { file_id type full_url signed_url }
  }
}
```

- 事件来自承运商时，`is_third_party_tracking` 为 true。
- `data`：最新事件在前。请根据 `tracking_event_status_id` / `otep_status` 进行分支判断，而不是 `description`。在承运商扫描包裹之前，早期事件可能仍为“信息已提交”。
- `deliveried` 为 true 且 `500` 表示已送达；此时 `proofs[]` 可能包含签名（`type` `1`）或照片（`type` `2`）。

**验证：** 查询返回您刚创建的货件。未知或已取消的单号返回 `404` 及 `result: false`。

## 11. 配置事件通知

Webhook 取代轮询：网店的服务器接收承运商的每次扫描并更新订单，无需定时调用第 10 步。

| 设置项 | 事件 | 触发时机 |
|---|---|---|
| `tracking_event_webhook_url` | `tracking.event` | 承运商扫描、派送中、已送达 |
| `order_status_change_webhook_url` | `order.status_change` | 您系统中的状态 |
| `order_create_webhook_url` | `order.created` | 已创建面单订单（第 7 步）；仅当 `order_created_webhook_all_types` 为 `1` 时才会针对面单订单发送 |

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

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

```json
{
  "result": true,
  "changed_keys": [
    "tracking_event_webhook_url",
    "order_create_webhook_url",
    "order_created_webhook_all_types",
    "webhook_sign_secret",
    "webhook_verify_ssl"
  ],
  "recipient_type": "business",
  "settings": {
    "webhook_sign_secret": "************CRET",
    "tracking_event_webhook_url": "https://shop.northbound-outfitters.ca/hooks/superroute",
    "order_create_webhook_url": "https://shop.northbound-outfitters.ca/hooks/superroute",
    "order_created_webhook_all_types": 1,
    "webhook_verify_ssl": 1
  }
}
```

- 只更改您发送的键；未知的键将以 `400` 拒绝。
- `changed_keys` 列出已保存的内容。密钥始终以掩码形式返回。

**GraphQL：** `webhookSettingsUpdate`（[GraphQL 手册](/api/graphql/documentation#/webhooks/webhookSettingsUpdate)）。

每个 `tracking.event` 都带有 `order_id`、`tracking_event_status_id`、`tracking_event_key`、`tracking_number` 和 `external_tracking_number`；按 `order_id`（第 7 步的 `id`）与您的订单对应。

对原始请求体校验 **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;
}
```

第 12 步的面单取消不会发送 `order_cancel_failed_webhook_url`（`order.cancel_failed`）；面单取消被拒绝时，会在该调用的响应中返回。

**验证：** 一次测试 `submitOrder` 产生带有订单 `id` 的 `order.created`，承运商的第一次扫描产生 `tracking.event`。接收端必须以 `401` 拒绝无效签名。

## 12. 取消

不再寄出的面单应取消，以免承运商计费；费用将退回账号。只有在承运商仍允许取消时才能取消（通常在揽收之前）。

**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" \
  -H "Idempotency-Key: nb-10482-cancel" \
  -d '{"id": 128455}'
```

```json
{
  "result": true,
  "message": "Shipping Label cancelled successfully"
}
```

- `id`（订单 id）与 `tracking_number`（Superroute 或承运商运单号）只能且必须发送其中一个。两者都发送时返回 `400`。
- `result: true`：承运商已接受取消，面单费用已退回。

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

承运商已收取包裹时会拒绝取消：响应为 `400`，带有 `result: false` 及承运商的消息。从未购买面单的订单无法通过本调用取消。

**验证：** 响应为 `result: true`，且该单号的公开追踪返回 `404`。使用相同的 `Idempotency-Key` 重试时返回已保存的响应；对同一订单发起新的取消请求时返回 `400` `This order already cancelled`。

## 13. 提交货件信息并日终结算（仅当该运输方式要求时）

部分承运商要求在揽收前传送当天的货件（交接清单）。第 8 步在每张订单的 `needSubmitShippingInformation` 中报告这一点。请在当天收集这些订单 id，并在最后一张面单之后提交。

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/submitShippingInformation \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ids": [128455, 128461, 128470]}'
```

```json
{
  "result": true,
  "message": "Processed 3 orders. Success: 3, Failed: 0",
  "data": {
    "total_processed": 3,
    "success_count": 3,
    "failure_count": 0,
    "details": [
      { "order_id": 128455, "result": true, "message": "Successful" },
      { "order_id": 128461, "result": true, "message": "Successful" },
      { "order_id": 128470, "result": true, "message": "Successful" }
    ]
  }
}
```

- `details[]`：每张订单一行；对 `result: false` 的订单，按 `message` 修正原因后重新提交。
- `404` `No eligible orders found for shipping information submission`：所列 id 中没有已购买面单且仍需提交的订单。

**GraphQL：** `labelserviceSubmitShippingInformation`（[GraphQL 手册](/api/graphql/documentation#/labelservice/labelserviceSubmitShippingInformation)）。

然后进行日终结算。本调用不需要请求体，覆盖调用方的所有面单订单。

**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"
```

```json
{
  "result": true,
  "success": 0,
  "failed": 0,
  "success_ids": [],
  "failed_ids": []
}
```

- `400` 并返回 `There are orders need to submit shipping information`：部分已购买的面单仍需提交；请通过 `submitShippingInformation` 提交后再次调用。

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

当天没有任何订单报告 `needSubmitShippingInformation: true` 时，跳过本步骤。

**验证：** `submitShippingInformation` 报告 `failure_count: 0`，且 `endofday` 返回 `200`。请先在测试运输方式上执行。

## 14. 错误处理

面单服务的错误带有 `message`；仅在表中列出时才带有 `code`。

| 情形 | HTTP 状态 | 代码 | 集成方的处理 |
|---|---|---|---|
| 令牌缺失或已过期，或未开通 API 访问 | `401` | —（`Unauthorized`） | 重新登录；若问题持续，请商户开通 API 访问 |
| 缺少 `shipping_method`，或调用方无权使用该运输方式 | `400` | — | 重新加载运输方式列表（第 5 步），并使用其中的 `id` |
| 该运输方式不提供此 `package_type` | `400` | — | 使用第 5 步 `package_type` 中的键 |
| 地址或包裹无效，或承运商未返回报价 | `400` | —（承运商消息） | 显示该消息，修正数据后重新询价 |
| `auto_deduplication` 为 `1` 且 `ref` 已存在 | `400` | —（`exist_order_ids`） | 使用 `exist_order_ids` 中的已有订单，而不是创建新订单 |
| 相同的 `Idempotency-Key` 用于不同的请求体 | `409` | `IDEMPOTENCY_CONFLICT` | 不同的请求使用新的键 |
| 第一次请求仍在处理中时使用相同的 `Idempotency-Key` | `409` | `IDEMPOTENCY_IN_PROGRESS` | 等待 `Retry-After` 秒后，使用相同的键和请求体重试 |
| 客户余额加信用额度不足以支付面单 | `400` | `INSUFFICIENT_BALANCE` | 按 `insufficient_balance` 详情（`shortfall`、`add_funds_url`）充值后，再次调用第 8 步 |
| 面单已购买，承运商文件尚未就绪 | 首次购买时为 `400`，之后为 `200` | `shipment_label_not_ready` | `labelStatus` 为 `pending` 时等待；为 `failed` 时再次调用第 8 步 |
| 订单 id 或单号不属于调用方 | `401` | —（`Not Auth`） | 检查 id 和 `type`；使用创建该订单的账号 |
| 承运商拒绝取消，或订单已取消 | `400` | — | 将该面单视为已寄出（或已取消）；不要重试 |
| 运单号未知或已取消 | `404` | — | 停止显示该单号的时间线 |
| 存在尚未提交的货件时调用 `endofday` | `400` | — | 为这些订单执行 `submitShippingInformation`，然后再次调用 |

## 验收清单

请使用您可控制的目的地和可以取消的运输方式：

- [ ] 运输方式列表不为空；您已记录一个 `id`。
- [ ] 询价为该运输方式和目的地返回价格。
- [ ] 提交返回订单 `id` 和 `rates[].rate_id`；相同的 `Idempotency-Key` 不会创建第二张订单。
- [ ] 使用所选 `rate_id` 调用 `getShippingDetail` 返回 `mainTrackingNumber`；第二次调用不会再次扣费。
- [ ] 面单 PDF 可以打开，并显示承运商运单号。
- [ ] 公开追踪可以通过该单号查到货件。
- [ ] 收到 `tracking.event`（以及启用时的 `order.created`）；v2 签名校验通过。
- [ ] 带有 `items` 的跨境测试货件被承运商接受。
- [ ] 取消成功，**或**您已确认该运输方式在下单后无法取消。
- [ ] 若该运输方式需要日终结算，测试执行无错误完成。
