> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.orderupb2b.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.orderupb2b.com/_mcp/server.

# 批次端點

客戶、商品、規格（SKU）、庫存各自提供一支批次端點，讓你一次寫入多筆資料，不必逐筆呼叫單筆端點。四支批次端點共用同一套「先擋空批次、受理後一律 200、逐筆各自回報成功或失敗」的模型，但逐筆狀態的字典分成兩組，混用會直接讓你的解析邏輯出錯——這是本頁的重點。

## 空陣列或沒有 body 會先被拒絕

送出空陣列 `[]`，或請求完全沒有 body，會在受理前就被直接擋下，回傳 `400 empty_batch`。這個檢查發生在「受理」這個動作之前——被擋下的請求不算數，不會出現在下一節「一律 200」的規則裡。

四支批次端點都遵守同一條規則：

* `POST /v1/customers/batch`
* `POST /v1/products/batch`
* `POST /v1/skus/batch`
* `POST /v1/skus/stock/batch`

## 已受理的請求一律回傳 HTTP 200

只要陣列不是空的，這個請求就算已受理——批次端點永遠回傳 HTTP `200`，即使陣列裡有一筆、甚至全部都處理失敗。逐筆的處理結果放在回應 body 的 `items` 裡，順序與送出的陣列相同；一筆失敗不會讓其他筆一起失敗，也不會讓整個請求失敗或改變外層的 HTTP 狀態碼。

`201`、`404`、`409` 這三個狀態碼只會出現在單筆端點（例如 `POST /v1/customers`、`PUT /v1/products/{id}`、`PUT /v1/internal-users/{id}`）。批次端點不會回傳它們——批次端點對應的成功或失敗結果，一律藏在 200 回應的 `items` 裡，不會反映在 HTTP 狀態碼上。判斷批次裡某一筆有沒有成功，永遠是看那一筆的 `status`，不是看外層的 HTTP 狀態碼。

## 兩套逐筆狀態字典，不可混用

批次端點分兩種家族，逐筆結果的欄位形狀與狀態值完全不同：

| 端點                          | 狀態字典                         | 識別欄位    | 錯誤欄位              |
| --------------------------- | ---------------------------- | ------- | ----------------- |
| `POST /v1/customers/batch`  | `created`／`updated`／`failed` | `index` | `errors`（陣列）      |
| `POST /v1/products/batch`   | `created`／`updated`／`failed` | `index` | `errors`（陣列）      |
| `POST /v1/skus/batch`       | `created`／`updated`／`failed` | `index` | `errors`（陣列）      |
| `POST /v1/skus/stock/batch` | `ok`／`failed`                | `skuId` | `error`（字串或 null） |

### 主檔批次：`created`／`updated`／`failed`

客戶、商品、規格三支主檔批次端點，逐筆結果的 `status` 只有三種小寫值：

| `status`  | 意義                                                            |
| --------- | ------------------------------------------------------------- |
| `created` | 這一筆新增成功                                                       |
| `updated` | 這一筆更新成功——依 `id` 直接更新，或依 `outerSysCode` 去重更新，判斷規則見「Upsert 與冪等」 |
| `failed`  | 這一筆失敗，原因在 `errors` 裡                                          |

**`POST /v1/products/batch 的回應`**

```json title="POST /v1/products/batch 的回應"
{
  "items": [
    { "index": 0, "status": "created", "id": 101, "errors": [] },
    { "index": 1, "status": "updated", "id": 42, "errors": [] },
    { "index": 2, "status": "failed", "id": null, "errors": ["invalid_product_category_id"] }
  ]
}
```

`index` 是這一筆在送出陣列中的位置（從 0 起算）；`id` 在成功時是新增或更新後的資料 id，失敗時是 `null`；`errors` 在失敗時可能不只一則，成功時是空陣列。

### 庫存批次：`ok`／`failed`

`POST /v1/skus/stock/batch` 是唯一的庫存批次端點，逐筆結果的 `status` 只有兩種小寫值，跟主檔批次完全不同，也沒有 created／updated 的區分——因為庫存的寫入永遠是覆蓋（絕對值快照），不是新增：

| `status` | 意義                  |
| -------- | ------------------- |
| `ok`     | 這一筆庫存更新成功           |
| `failed` | 這一筆失敗，原因在 `error` 裡 |

**`POST /v1/skus/stock/batch 的回應`**

```json title="POST /v1/skus/stock/batch 的回應"
{
  "items": [
    { "skuId": 1, "status": "ok", "error": null },
    { "skuId": 2, "status": "failed", "error": "SKU not found" }
  ]
}
```

欄位名稱也不一樣：庫存批次用 `skuId` 識別、`error` 回報失敗原因（單一字串或 `null`）；主檔批次用 `index` 識別、`errors` 回報失敗原因（陣列）。如果你打算寫一支共用的批次結果解析器同時處理主檔與庫存批次，這兩點差異都要處理，不能假設兩者共用同一個形狀。

## 下一步

#### [Upsert 與冪等](/upsert)

批次逐筆套用的新增／更新／去重規則，以及部分合併的細節

#### [錯誤碼](/errors)

`errors`／`error` 裡可能出現的失敗原因，以及完整的錯誤代碼對照表