> 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.

# 分頁與篩選

大部分清單端點共用同一套分頁與篩選規則，這裡一次講清楚；各端點頁面只補充自己特有的篩選欄位。

## 回應形狀

清單端點的回應都是同一個外層：

```json
{
  "data": [ ... ],
  "totalCount": 137,
  "currentPage": 1,
  "pageSize": 100,
  "totalPages": 2
}
```

`data` 是本頁的資料；`totalCount` 是符合篩選條件的總筆數，涵蓋所有頁，不受目前頁碼影響；`currentPage` 就是請求帶入的 `page`；`pageSize` 是請求帶入的 `pageSize`；`totalPages` 等於 `totalCount` 除以 `pageSize` 後無條件進位，沒有符合的資料時為 0。

## page 與 pageSize

`page` 從 1 起算。`pageSize` 的範圍是 1 到 100，省略時預設為 100。

**超出範圍會回傳 `400`，不會被靜默夾擠成合法值。** 帶 `pageSize=500` 不會被悄悄改成 100 再繼續執行，而是直接拒絕整個請求：

| 情況                     | 回應                      |
| ---------------------- | ----------------------- |
| `page` 小於 1            | `400 invalid_page`      |
| `pageSize` 不在 1–100 之間 | `400 invalid_page_size` |
| `page` 過大導致內部位移量溢位     | `400 invalid_page`      |

這個設計是刻意的：如果超出範圍的 `pageSize` 被自動夾回合法值，一支寫死「送 `pageSize=500`、跑到 `data` 回傳筆數小於 500 就當作最後一頁」的掃描程式，會在完全沒有察覺的情況下把每一頁都當成 100 筆處理，進度計算全部跑偏，卻不會出現任何錯誤——資料看起來像是抓完了，其實漏了一大截。改成直接回 `400`，任何寫錯分頁參數的介接程式都會在第一次呼叫就發現問題，而不是在資料對不上帳的時候才發現。

## updatedSince：增量同步用的篩選條件

支援增量同步的清單端點都能用 `updatedSince` 篩「異動時間等於或晚於此刻」的資料。**這個邊界是含在內的**：異動時間剛好等於你送出的那一刻，該筆資料也會被回傳，不是嚴格的「在此之後」。所以把上一輪掃到的最後一個異動時間直接當成下一輪的 `updatedSince`，交界處那幾筆會再出現一次——這是預期行為，你端依 id 覆寫即可，不要當成資料異常。

這個值必須是 ISO-8601 格式，並且**帶明確的時區位移或 `Z`**——`2026-07-01T00:00:00Z` 合法，`2026-07-01T00:00:00`（沒有位移、沒有 `Z`）會回傳 `400 invalid_updated_since`，不會被當成任何時區的預設猜測值。

放進網址時要留意百分號編碼：ISO-8601 的 `+`（例如 `+08:00` 時區）在 URL 裡有特殊意義，必須編碼成 `%2B`，否則會被解讀成空白字元；冒號通常也建議編碼成 `%3A`，避免部分用戶端函式庫或代理誤解析。例如：

```text
updatedSince=2026-07-01T08%3A00%3A00%2B08%3A00
```

或使用 UTC 的 `Z` 結尾，不含 `+`，可以省去這個顧慮：

```text
updatedSince=2026-07-01T00%3A00%3A00Z
```

`updatedSince` 進入簽章計算的方式跟其他 query 參數完全相同——canonical string 簽的是實際送出的、已經編碼過的那個字串（含 `%3A`、`%2B`），不是解碼後的可讀時間。細節見「驗證與簽章」的 path 與 query string 一節。

## outerSysCode／name／barcode 的比對方式

支援這些欄位篩選的清單端點上，比對規則並不一致，混用時要特別注意：

* **`outerSysCode`、`name`：大小寫不敏感的子字串比對。** 查 `A100` 也會比對到 `A1000`，因為 `A1000` 這個字串裡包含 `A100`。如果你只想找到剛好等於某個值的那一筆，篩選之後還是要在自己這端再比對一次完整字串。
* **`barcode`：精確比對。** 只有整段條碼完全相符才會命中，不是子字串比對，這一點與 `outerSysCode`／`name` 不同。

## GET /v1/product-categories 的分頁是例外

商品分類樹的分頁單位是**根分類**，不是分類列表裡的每一筆節點，跟其他清單端點不一樣：

* `data` 最多回傳 `pageSize` 個根分類，每個根分類底下帶著它**全部**的子分類——子分類永遠不會被拆到下一頁，一個根分類要嘛整個出現在這一頁，要嘛完全不出現。
* `totalCount` 算的是根分類的數量，不是分類總數（根分類數＋子分類數）。
* 因此一頁實際回傳的節點數是「`pageSize` 個根分類，加上它們各自帶的子分類」，不會剛好等於 `pageSize`。

商品分類樹通常只有兩層、根分類數十個，預設分頁一次通常就能取回整棵樹，建議查一次後在你端快取，不必每次建立或更新商品前都重新查詢。

## GET /v1/internal-roles 不分頁

後台角色清單沒有 `data`／`totalCount`／`currentPage`／`pageSize`／`totalPages` 這個分頁外層，回應就是一個 `data` 陣列，裡面是這個租戶可以指派的完整角色清單。一個租戶的角色數量少且固定，這支端點的用途是在建立後台使用者前查一次、在你端快取，不需要分頁機制。