# Phân trang và đồng bộ

Các endpoint danh sách phân trang bằng **con trỏ**, không dùng số trang.

## Con trỏ

| Tham số | Ý nghĩa |
| --- | --- |
| `limit` | Số dòng mỗi trang, từ 1 đến 100, mặc định 50. |
| `cursor` | Giá trị `nextCursor` của trang trước. Bỏ trống để đọc từ đầu. |

Phản hồi có dạng:

```json
{
  "data": [ ],
  "nextCursor": "eyJ2IjoxLCJ0IjoiMjAyNi0xMC0wMVQwODozMDowMC4wMDBaIiwiaWQiOiIuLi4ifQ"
}
```

`nextCursor` là `null` khi hết dữ liệu. Để đọc hết một danh sách, lặp lại với `cursor` mới (giữ nguyên các tham số khác) cho tới khi `nextCursor` là `null`. Con trỏ chỉ để lật trang trong CÙNG một lượt đọc: đừng lưu nó để nối lượt đồng bộ sau — việc ấy dùng [`updatedSince`](#dong-bo-tang-dan-voi-updatedsince).

`cursor` là chuỗi không trong suốt: đừng tự dựng hay phân tích nó, vì cấu trúc bên trong có thể đổi. Cursor sai hình dạng nhận 400 [`invalid-cursor`](/developers/errors#invalid-cursor).

## Thứ tự

Các danh sách đồng bộ được (đơn hàng, sản phẩm, khách hàng, vận đơn) sắp theo `(updatedAt, id)` **tăng dần**: bản ghi cũ trước, mới sau, và mỗi dòng đều mang `updatedAt` để bạn lấy mốc nối tiếp. Nhờ vậy bạn xử lý theo thứ tự thời gian; giữa hai lượt đồng bộ, thứ cần lưu là mốc `updatedAt`, không phải con trỏ (xem bên dưới).

Tồn kho cũng đồng bộ tăng dần nhưng khác một chút: `GET /inventory/stock` trả tồn của từng mẫu mã tại từng kho, sắp theo lần đổi tồn tăng dần, có `updatedSince` và `nextCursor` nhưng **dòng tồn không mang `updatedAt`** và không có `includeDeleted`. Với `updatedSince`, dùng thời điểm bạn bắt đầu lượt đồng bộ trước (lùi 15 phút, xem bên dưới) làm mốc. Mốc của dòng tồn chỉ đổi khi số tồn hoặc số đang về kho (`inTransit`) đổi. Giá, mã, mã vạch, tên sản phẩm, tên mẫu mã, ảnh và tên kho trong dòng tồn chỉ là thông tin đọc kèm: shop đổi chúng thì dòng tồn KHÔNG hiện lại trong `updatedSince`. Hãy lấy các trường ấy từ đồng bộ sản phẩm và danh sách kho.

Hai danh sách còn lại có phân trang nhưng không phục vụ đồng bộ tăng dần:

- `GET /conversations`: hội thoại có tin mới nhất trước; có `updatedSince` nhưng nó lọc theo `lastMessageAt` — giờ của tin mới nhất theo kênh (Facebook, Zalo), không phải mốc sửa — nên tin tới muộn có thể mang giờ trước mốc bạn đã đọc, còn đổi thẻ hay đánh dấu đã đọc không làm hội thoại hiện lại; không có `includeDeleted`.
- `GET /conversations/{id}/messages`: tin mới nhất trước; không có `updatedSince` và `includeDeleted`.

Danh mục (`/categories`), kho (`/warehouses`), trang chat (`/pages`), thẻ (`/tags`) và danh mục hành chính (`/geo/units`, mỗi lượt một tầng) không phân trang: đó là các danh sách nhỏ, trả trọn trong `data`, không có `nextCursor`. Con trỏ chỉ dành cho các danh sách lớn dần theo thời gian.

## Đồng bộ tăng dần với `updatedSince`

Để giữ một bản sao dữ liệu ở hệ thống của bạn, đừng đọc lại toàn bộ mỗi lần. Dùng `updatedSince`:

```bash
curl "https://danix.vn/api/open/v1/orders?updatedSince=2026-10-01T00:00:00.000Z&limit=100" \
  -H "Authorization: Bearer dnx_live_…"
```

`updatedSince` lọc các bản ghi có `updatedAt` từ mốc đó trở đi (thời gian ISO 8601, UTC). Lưu mốc `updatedAt` lớn nhất đã xử lý, lần sau truyền nó làm `updatedSince`. Đổi số điện thoại, email hay địa chỉ của khách, hoặc thêm, sửa, gỡ ảnh của sản phẩm, cũng nâng `updatedAt` của khách hay sản phẩm ấy, nên các thay đổi đó cũng hiện qua `updatedSince`.

Hai điều cần biết để không mất dữ liệu:

- **Độ trễ an toàn.** Khi gọi kèm `updatedSince`, danh sách đơn hàng, sản phẩm, khách hàng, vận đơn và tồn kho chỉ trả các bản ghi có `updatedAt` cũ hơn một khoảng trễ so với lúc gọi. Khoảng trễ ấy bằng thời gian tối đa mà một thao tác tính lại kho của shop được phép chạy (mặc định 2 phút, tối đa 10 phút, theo cài đặt của shop) cộng thêm 15 giây cho bước hoàn tất giao dịch — tức 2 phút 15 giây với cài đặt mặc định, và không bao giờ quá 10 phút 15 giây. Lý do: `updatedAt` là lúc giao dịch BẮT ĐẦU, còn bản ghi chỉ hiện ra lúc giao dịch commit. Sửa một phiếu kho đã ghi sổ hay đổi trạng thái đơn kéo theo tính lại giá vốn có thể chạy vài phút trong một giao dịch; nếu danh sách trả ngay, bạn đã đọc qua mốc ấy trước khi dòng commit và sẽ không bao giờ thấy nó. Không có `updatedSince` thì KHÔNG có độ trễ: danh sách trả cả dòng vừa sửa, nên đọc như vậy (kể cả lật tiếp bằng một con trỏ đã lưu từ lượt trước) không an toàn để đồng bộ tăng dần. Lượt đồng bộ đầu tiên cũng nên truyền `updatedSince` — một mốc thật xa như `2000-01-01T00:00:00Z` — thay vì bỏ trống, để lượt ấy cũng có độ trễ.
- **Chồng lấn 15 phút và khử trùng.** Mỗi lần đồng bộ, đặt `updatedSince` lùi lại 15 phút so với mốc đã lưu, rồi khử trùng theo `id` ở phía bạn (bản ghi nào gặp lại thì ghi đè bằng bản mới nhất). Chồng lấn phải dài hơn khoảng trễ an toàn tối đa (10 phút 15 giây); nó là lưới cho các ca biên mà khoảng trễ chưa phủ.

`updatedSince` là đường **đối soát**. Muốn nhận thay đổi gần thời gian thực, dùng luồng tự động và [webhook](/developers/webhooks).

## Dòng đã xoá

DANIX xoá mềm bản ghi theo mặc định, nên danh sách thường không có chúng. Thêm `includeDeleted=true` để nhận cả dòng đã xoá; mỗi dòng ấy mang `deletedAt`. Khi đồng bộ, hãy bật tham số này, nếu không bản sao của bạn không bao giờ biết một bản ghi đã bị xoá.

`includeDeleted` áp cho đơn hàng, sản phẩm và khách hàng; dòng sản phẩm đã xoá mang `deletedAt` ngay ở danh sách. Vận đơn và tồn kho không có tham số này, có lý do:

- **Vận đơn** chỉ bị xoá khi hãng vận chuyển từ chối ngay lúc tạo: dòng ấy chưa từng có mã vận đơn và chưa từng tới hãng, tồn tại vài giây rồi bị xoá — ngắn hơn khoảng trễ an toàn nên bản sao của bạn gần như không bao giờ nhận được nó. Danh sách vận đơn luôn bỏ vận đơn đã xoá. Vận đơn bị huỷ không bị xoá: nó ở lại với `status` là `cancelled`.
- **Tồn kho** không bao giờ bị xoá. Khi mẫu mã, sản phẩm hay kho bị xoá, dòng tồn của nó ra khỏi danh sách; bạn biết điều ấy qua đồng bộ sản phẩm (`includeDeleted=true`) và danh sách kho.

Xem bảng tham số từng endpoint ở phần tham chiếu.
