Nguồn: https://danix.vn/developers.md

# Tài liệu nhà phát triển DANIX

Open API của DANIX cho phần mềm bên ngoài làm việc với dữ liệu của MỘT shop: đọc và ghi đơn hàng, sản phẩm, kho, khách hàng, vận đơn và hội thoại chat, rồi nhận dữ liệu mới ngay khi có thay đổi thông qua luồng tự động. Tài liệu này viết bằng tiếng Việt cho người viết tích hợp và cho các trợ lý AI giúp họ; mỗi trang có bản Markdown thuần ở cùng địa chỉ với đuôi `.md`.

Có hai cách làm việc, và thường dùng cùng nhau:

- **Gọi API.** Phần mềm của bạn gửi request tới `https://danix.vn/api/open/v1/…` kèm một khoá API. Mọi request thuộc về đúng shop sở hữu khoá.
- **Nhận dữ liệu.** Trong ứng dụng Tự động hoá của shop, bạn dựng một luồng: khi có sự kiện (đơn mới, tin nhắn mới…) luồng gửi dữ liệu tới địa chỉ HTTPS của bạn, ký bằng chữ ký để bạn kiểm tra. Xem [Luồng tự động và webhook](/developers/webhooks).

## Bắt đầu nhanh

### 1. Tạo ứng dụng kết nối và lấy khoá

Quản trị viên shop (hoặc người có quyền `automation.connections.manage`) mở ứng dụng **Tự động hoá**, vào mục **Kết nối API** và tạo một "ứng dụng kết nối". Khi tạo, bạn chọn đúng những quyền ứng dụng cần, và với chat thì chọn thêm các trang Facebook hay Zalo mà ứng dụng được làm việc. Bạn chỉ chọn được quyền và trang mà chính bạn đang có.

Khoá hiện **đúng một lần** khi tạo (hoặc khi xoay), có dạng `dnx_live_` kèm 38 ký tự. Hãy lưu nó ngay vào nơi giữ bí mật của hệ thống bạn. Chi tiết về quyền, xoay và thu hồi: [Xác thực](/developers/authentication).

### 2. Gọi thử bằng curl

```bash
curl "https://danix.vn/api/open/v1/shop" \
  -H "Authorization: Bearer dnx_live_…"
```

Thay `dnx_live_…` bằng khoá của bạn. Phản hồi có dạng sau (rút gọn); nhận được nó là thành công: khoá hợp lệ và shop còn dùng được.

```json
{
  "id": "…",
  "slug": "ten-shop",
  "name": "Tên shop",
  "application": {
    "name": "Tên ứng dụng kết nối",
    "permissions": ["pos.orders.read"],
    "pageIds": []
  },
  "readOnly": false
}
```

`application.permissions` liệt kê quyền khoá đang có, và `readOnly` là `true` khi gói của shop hết hạn (khoá chỉ còn đọc được). Nếu nhận lỗi, đối chiếu mã `code` ở trang [Lỗi](/developers/errors); lỗi thường gặp nhất là `invalid-api-key` (sai khoá) và `insufficient-permission` (khoá chưa được cấp quyền cho đường vừa gọi).

Đọc danh sách đơn hàng (cần quyền `pos.orders.read`):

```bash
curl "https://danix.vn/api/open/v1/orders?limit=20" \
  -H "Authorization: Bearer dnx_live_…"
```

Phản hồi là `{ "data": [...], "nextCursor": "..." }`; lặp lại với `cursor=<nextCursor>` để đọc trang kế. Xem [Phân trang và đồng bộ](/developers/pagination).

### 3. Tạo một luồng gửi dữ liệu tới địa chỉ thử

1. Trong ứng dụng Tự động hoá, mở mục **Luồng tự động** và bấm **Tạo luồng**.
2. Trên bảng vẽ, giữ khối **Kích hoạt** và chọn sự kiện, ví dụ `order.created`.
3. Thêm khối **Gửi HTTP**, nối dây từ Kích hoạt sang nó. Điền địa chỉ HTTPS của bên nhận (một địa chỉ thử như dịch vụ nhận webhook công cộng, hoặc máy chủ của bạn), phương thức `POST`.
4. Bấm **Chạy thử** để gửi một sự kiện mẫu, rồi **Lưu** và bật luồng.

Mỗi lượt gửi mang chữ ký theo chuẩn [Standard Webhooks](https://www.standardwebhooks.com/). Trước khi tin nội dung, hãy kiểm chữ ký bằng mã mẫu ở trang [Luồng tự động và webhook](/developers/webhooks). Người đã quen n8n nên đọc thêm [Cách luồng chạy](/developers/flow-logic).

## Đọc tiếp

- [Xác thực](/developers/authentication): khoá, quyền, xoay và thu hồi.
- [Lỗi](/developers/errors): định dạng lỗi và bảng mã.
- [Hạn mức gọi](/developers/rate-limits), [Phân trang và đồng bộ](/developers/pagination), [Idempotency-Key](/developers/idempotency).
- [Luồng tự động và webhook](/developers/webhooks), [Cách luồng chạy (cho người quen n8n)](/developers/flow-logic), [Sự kiện](/developers/events).
- [Phiên bản](/developers/versioning).
- Tham chiếu từng endpoint: [Shop](/developers/reference/shop), [Đơn hàng](/developers/reference/orders), [Sản phẩm](/developers/reference/products), [Kho](/developers/reference/inventory), [Khách hàng](/developers/reference/customers), [Địa giới hành chính](/developers/reference/geo), [Vận đơn](/developers/reference/shipments), [Hội thoại](/developers/reference/chat).
- Đặc tả máy đọc: [openapi.json](/openapi.json). Cho trợ lý AI: [llms.txt](/llms.txt) và [llms-full.txt](/llms-full.txt).

> Không có nút "gọi thử" trên trang này và API không mở CORS: khoá API không bao giờ được đặt trong trình duyệt. Hãy gọi từ máy chủ của bạn.

---

Nguồn: https://danix.vn/developers/authentication.md

# Xác thực

Mọi request tới `/api/open/v1` phải mang khoá API trong header `Authorization`:

```bash
curl "https://danix.vn/api/open/v1/shop" \
  -H "Authorization: Bearer dnx_live_…"
```

Khoá thiếu, sai định dạng, đã thu hồi hoặc đã hết hạn nhận 401 với `code` là `invalid-api-key`. Một khoá API không dùng được làm phiên đăng nhập của người dùng: đường dành cho phiên nhận 401 "chưa đăng nhập", riêng đăng xuất và đăng ký thông báo đẩy nhận 400 `api-key-not-accepted` để nói rõ lý do.

## Khoá API

- Dạng `dnx_live_` kèm 38 ký tự chữ và số, 6 ký tự cuối là mã kiểm. Có thể dò khoá bị lộ bằng biểu thức `\bdnx_live_[0-9A-Za-z]{38}\b`.
- Mỗi khoá thuộc về **một shop** qua một "ứng dụng kết nối". Khoá của shop X không bao giờ đọc hay ghi được dữ liệu của shop Y, kể cả khi bạn gửi đúng mã của một bản ghi thuộc shop Y: lớp chặn nằm ở cơ sở dữ liệu, không chỉ ở API.
- DANIX chỉ lưu giá trị băm của khoá. Khoá nguyên văn hiện đúng một lần, lúc tạo hoặc lúc xoay. Mất khoá thì xoay khoá để lấy khoá mới.
- Khoá chỉ đi trong header `Authorization`. Không đặt khoá trong địa chỉ URL, trong tham số query hay trong mã chạy ở trình duyệt; API cũng không mở CORS.

## Quyền

Khi tạo ứng dụng kết nối, bạn chọn tập quyền. Khoá chỉ gọi được những đường mà tập quyền ấy cho phép, và đường nào cần quyền gì được ghi ở mục **Quyền cần có** của từng endpoint trong phần tham chiếu. Thiếu quyền nhận 403 với `code` là `insufficient-permission`.

Vài trường của thân yêu cầu cần thêm quyền riêng, ghi ở mục **Quyền thêm theo trường** của endpoint: tạo hay sửa đơn với `unitPrice` khác giá niêm yết của mẫu mã, hay giảm giá (của dòng hoặc cả đơn) khác 0, cần `pos.orders.price.override` (và shop không bật khoá sửa giá trong cài đặt bán hàng); tạo đơn kèm `payments` cần `pos.orders.payment.record`; đổi trạng thái để huỷ một đơn đã gửi hàng (từ `shipped`, `delivered`, `paid`, `returning`, `partially_returned` hay `returned` sang `cancelled` — hàng khách đang giữ được nhập lại kho) cần `pos.orders.delete`; tạo sản phẩm có `variants[].attributes` mang tên thuộc tính hay giá trị CHƯA có trong shop (hệ thống tạo chúng) cần `pos.products.update`. Khoá thiếu quyền ấy vẫn tạo được đơn theo giá niêm yết, không giảm giá, chưa thu tiền, vẫn huỷ được đơn chưa gửi hàng, và vẫn tạo được sản phẩm dùng thuộc tính đã có.

Giới hạn khi cấp quyền:

- Ứng dụng không được có nhiều quyền hơn chính người đang tạo hay sửa nó, và điều này được kiểm lại **mỗi lần** người ấy xoay khoá, sửa quyền hay sửa trang.
- Không cấp được các quyền quản trị shop (`pos.admin.*`) và quyền của chính ứng dụng Tự động hoá (`automation.*`).
- Một shop có tối đa 10 ứng dụng kết nối.

## Trang chat được chọn

Với các endpoint chat, ứng dụng kết nối mang kèm danh sách **trang** Facebook hoặc Zalo mà người tạo chọn. Khoá chỉ thấy và chỉ gửi tin được trên các trang ấy; một trang ngoài danh sách trả 404 `not-found` như thể nó không tồn tại. Người tạo chỉ chọn được trang mà chính họ thấy.

## Xoay khoá

Xoay khoá cấp một khoá mới và cho khoá cũ sống thêm một **thời gian chồng** (mặc định 24 giờ, chọn từ 0 đến 72 giờ) để bạn thay khoá ở các hệ thống mà không phải ngừng chạy. Tối đa hai khoá cùng sống. Hết thời gian chồng, khoá cũ nhận 401.

Cấp thêm một khoá mà **không** xoay (ví dụ cho hệ thống thứ hai) thì khoá đang có giữ nguyên, không bị đặt hạn. Ứng dụng đã có đủ hai khoá sống thì không cấp thêm được: thu hồi một khoá trước.

Cách xoay không gián đoạn: tạo khoá mới, cập nhật nơi giữ bí mật của hệ thống, triển khai, theo dõi tới khi không còn request nào dùng khoá cũ, rồi để khoá cũ hết hạn hoặc thu hồi nó.

## Thu hồi

Thu hồi khoá có hiệu lực ngay: request kế tiếp dùng khoá ấy nhận 401, không có độ trễ bộ đệm. Màn hình **Kết nối** có nút thu hồi cho từng khoá; tạo, xoay và thu hồi đều được ghi vào nhật ký của shop, đứng tên người thao tác.

Khi người tạo ứng dụng rời shop, bị khoá tài khoản hay đổi mật khẩu, khoá **không tự bị thu hồi**: màn hình Kết nối gắn nhãn "Người tạo đã rời shop" hoặc "Tài khoản người tạo đã bị khoá" kèm nút xoay và thu hồi để quản trị viên quyết định.

## Khi shop không dùng được API

| Tình huống | Kết quả |
| --- | --- |
| Shop bị đình chỉ | Mọi request bị từ chối với 403 `shop-suspended`. |
| Gói của shop hết hạn (chỉ đọc) | Request đọc (`GET`) vẫn chạy; request ghi nhận 402 `subscription-expired`. Gia hạn gói là hết lỗi. |
| Gói của shop không có tính năng Tự động hoá | 403 `automation-not-active`. |

Danh sách đầy đủ các mã ở trang [Lỗi](/developers/errors).

## Giữ khoá an toàn

- Lưu khoá ở kho bí mật của hệ thống (biến môi trường của máy chủ, trình quản lý bí mật), không commit vào mã nguồn.
- Không in khoá ra log. Khi log request, che header `Authorization`.
- Cấp cho mỗi hệ thống một ứng dụng kết nối riêng với **đúng** quyền cần dùng, để thu hồi một nơi không kéo theo nơi khác.
- Nghi ngờ lộ khoá thì thu hồi ngay rồi tạo khoá mới.

---

Nguồn: https://danix.vn/developers/errors.md

# Lỗi

Mọi lỗi của `/api/open/v1` có dạng **RFC 9457** (`application/problem+json`) kèm trường `code` ổn định:

```json
{
  "type": "https://danix.vn/developers/errors#rate-limited",
  "title": "Vượt hạn mức gọi API, hãy chờ rồi thử lại",
  "status": 429,
  "code": "rate-limited",
  "detail": "Chờ 12 giây rồi thử lại."
}
```

| Trường | Ý nghĩa |
| --- | --- |
| `type` | Địa chỉ giải thích loại lỗi; phần neo `#…` chính là `code`. |
| `title` | Tên ngắn của loại lỗi, ổn định theo `code`. |
| `status` | Mã trạng thái HTTP. |
| `code` | Mã máy đọc, chữ thường nối bằng gạch ngang. **Dùng trường này để rẽ nhánh xử lý**, không dùng `title` hay `detail`. |
| `detail` | Mô tả cho đúng lượt lỗi này, dành cho người đọc; câu chữ có thể đổi. |
| `errors` | Chỉ có ở `validation-failed`, khi lỗi gắn với trường cụ thể (thân, query hay tham số đường dẫn sai hình dạng): danh sách trường sai, mỗi phần tử gồm `path` (đường tới trường, ví dụ `lines.0.quantity`) và `message` (vì sao sai). Từ chối nghiệp vụ không gắn với một trường (ví dụ mẫu mã không tồn tại trong shop) chỉ có `detail`. |

Client cần bỏ qua trường lạ trong thân lỗi, và coi `code` lạ như lỗi cùng nhóm với `status` của nó. Danh sách `code` có thể được bổ sung mà không đổi phiên bản.

## Cách xử lý theo nhóm

- **400, 413, 415 và 422**: request sai, thử lại y nguyên sẽ sai tiếp. Sửa request.
- **401 và 403**: vấn đề khoá hay quyền, không phải mạng. Đừng thử lại dồn dập; kiểm tra khoá và quyền.
- **402**: gói của shop hết hạn; chờ shop gia hạn.
- **404**: không có bản ghi, hoặc bản ghi thuộc shop khác, hoặc trang chat ngoài danh sách của khoá. Cố ý không phân biệt ba trường hợp này.
- **409**: xung đột trạng thái hoặc lượt trước còn chạy. Một số mã dặn thử lại sau một lúc.
- **429**: chờ theo `Retry-After`; xem [Hạn mức gọi](/developers/rate-limits).
- **5xx**: lỗi phía DANIX, hãng vận chuyển (503 `carrier-unavailable`) hoặc kênh chat (503 `channel-unavailable`). Thử lại với khoảng chờ tăng dần; dùng [Idempotency-Key](/developers/idempotency) để thử lại không tạo trùng.

## Bảng mã lỗi

### validation-failed {#validation-failed}

**400.** Thân, tham số query hoặc tham số đường dẫn không hợp lệ, hoặc dữ liệu hợp lệ về hình dạng nhưng không dùng được (ví dụ mã tham chiếu không có trong shop). Khi lỗi gắn với trường cụ thể, mảng `errors` chỉ rõ từng trường (`path` và `message`); từ chối không gắn với một trường chỉ có `detail`. Sửa theo đó rồi gửi lại.

### invalid-cursor {#invalid-cursor}

**400.** `cursor` sai hình dạng hoặc không phải giá trị API đã trả. Bắt đầu lại từ trang đầu, hoặc dùng đúng `nextCursor` của trang trước. Xem [Phân trang và đồng bộ](/developers/pagination).

### api-key-not-accepted {#api-key-not-accepted}

**400.** Bạn gửi khoá API tới đường đăng xuất hoặc đường đăng ký thông báo đẩy, vốn chỉ dành cho phiên đăng nhập của người dùng. Các đường khác ngoài `/api/open/v1` nhận 401 như khi chưa đăng nhập. Khoá API chỉ dùng cho `/api/open/v1`.

### invalid-api-key {#invalid-api-key}

**401.** Thiếu header `Authorization`, khoá sai định dạng, không tồn tại, đã thu hồi hoặc đã hết hạn. Kiểm tra khoá; nếu vừa xoay, dùng khoá mới. Không thử lại cùng khoá.

### subscription-expired {#subscription-expired}

**402.** Gói của shop đã hết hạn nên shop ở chế độ chỉ đọc; chỉ request ghi bị từ chối, request `GET` vẫn chạy. Shop gia hạn gói là hết lỗi.

### insufficient-permission {#insufficient-permission}

**403.** Khoá không có quyền cho đường này, hoặc thân yêu cầu dùng một trường cần quyền riêng (ví dụ đặt `unitPrice` khác giá niêm yết, giảm giá hay ghi `payments` khi tạo đơn). Quyền cần có — và "Quyền thêm theo trường" — ghi ở từng endpoint trong phần tham chiếu. Nhờ quản trị viên shop bổ sung quyền cho ứng dụng kết nối.

### shop-suspended {#shop-suspended}

**403.** Shop đang bị đình chỉ. Mọi request bị từ chối cho tới khi shop được mở lại.

### automation-not-active {#automation-not-active}

**403.** Gói của shop không có tính năng Tự động hoá nên khoá API không dùng được. Shop cần nâng gói.

### not-found {#not-found}

**404.** Không có bản ghi hoặc đường dẫn này với khoá của bạn. Cũng là kết quả khi bản ghi thuộc shop khác hoặc trang chat nằm ngoài danh sách được chọn cho khoá.

### conflict {#conflict}

**409.** Thao tác xung đột với trạng thái hiện tại (ví dụ đơn đã ở trạng thái không cho phép bước này). Đọc lại bản ghi rồi quyết định.

### idempotency-key-in-progress {#idempotency-key-in-progress}

**409.** Lượt trước dùng cùng `Idempotency-Key` vẫn đang chạy. Phản hồi trả ngay, kèm `Retry-After: 1`. Chờ rồi gửi lại **y nguyên** request; đừng đổi khoá. Xem [Idempotency-Key](/developers/idempotency).

### idempotency-key-reused {#idempotency-key-reused}

**422.** `Idempotency-Key` này đã dùng cho một request có thân khác. Mỗi khoá chỉ gắn với một nội dung; dùng khoá mới cho nội dung mới.

### payload-too-large {#payload-too-large}

**413.** Thân yêu cầu vượt quá dung lượng máy chủ nhận. Thân của mọi endpoint đều nhỏ (một đơn tối đa 200 dòng hàng); gặp mã này là request đang gửi thứ không cần thiết. Thu gọn thân, đừng thử lại y nguyên.

### unsupported-media-type {#unsupported-media-type}

**415.** Thân yêu cầu không phải JSON. Gửi kèm header `Content-Type: application/json` và thân JSON hợp lệ.

### rate-limited {#rate-limited}

**429.** Vượt hạn mức của khoá. Chờ đủ thời gian trong header `Retry-After` (số giây) rồi thử lại. Xem [Hạn mức gọi](/developers/rate-limits).

### internal-error {#internal-error}

**500.** Lỗi phía DANIX. Thử lại với khoảng chờ tăng dần; nếu lỗi kéo dài, liên hệ hỗ trợ kèm thời điểm và đường dẫn đã gọi.

### carrier-unavailable {#carrier-unavailable}

**503.** Hãng vận chuyển hiện không với tới được (mạng, hãng đang lỗi hoặc kết nối của shop chưa dùng được), nên thao tác tạo hay huỷ vận đơn chưa hoàn tất. Thử lại sau một lúc với khoảng chờ tăng dần; trước khi tạo lại, đọc `GET /shipments?orderId=…` để biết lượt trước đã để lại vận đơn nào chưa. Lỗi kéo dài thì báo quản trị viên shop kiểm tra kết nối hãng.

### channel-unavailable {#channel-unavailable}

**503.** Kênh chat (Facebook, Zalo) tạm thời không phản hồi — kênh đang giới hạn tốc độ gọi của trang, máy chủ của kênh lỗi hoặc mạng chập chờn — nên thao tác gửi tin, nhắn riêng hay ẩn, hiện bình luận chưa hoàn tất. Khác `rate-limited`: đây không phải hạn mức của khoá API. Thử lại sau một lúc với khoảng chờ tăng dần.

### order-has-live-shipment {#order-has-live-shipment}

**409.** Đơn còn vận đơn chưa kết thúc nên không thực hiện được thao tác này (ví dụ sửa nội dung hàng). Huỷ hoặc chờ vận đơn kết thúc, rồi thao tác lại.

### order-status-not-allowed {#order-status-not-allowed}

**422.** Không chuyển được đơn sang trạng thái này từ trạng thái hiện tại. Đọc lại đơn để biết trạng thái hiện hành rồi chọn bước hợp lệ.

### insufficient-stock {#insufficient-stock}

**422.** Không đủ tồn kho cho thao tác này. Kiểm tra tồn kho bằng `GET /inventory/stock`, giảm số lượng hoặc nhập thêm hàng rồi thử lại.

### variant-removed {#variant-removed}

**422.** Một mẫu mã trong request đã bị gỡ khỏi sản phẩm. Lấy lại sản phẩm để biết mẫu mã còn hiệu lực.

### duplicate-code {#duplicate-code}

**409.** Mã bạn đặt (mã đơn, mã sản phẩm…) đã tồn tại trong shop. Chọn mã khác.

### customer-blocked {#customer-blocked}

**422.** Khách hàng đang bị chặn nên không thực hiện được thao tác này.

### document-already-posted {#document-already-posted}

**409.** Phiếu kho đã ghi sổ rồi nên không ghi sổ lại hay sửa được.

### shipment-not-cancellable {#shipment-not-cancellable}

**409.** Vận đơn không còn huỷ được ở trạng thái hiện tại. Đọc `GET /shipments/:id` để xem trạng thái và hành trình.

### messaging-window-closed {#messaging-window-closed}

**422.** Đã quá khung thời gian nền tảng cho phép nhắn cho khách này (ví dụ 24 giờ với Facebook). Chỉ gửi lại được khi khách nhắn tin mới.

### page-not-connected {#page-not-connected}

**422.** Trang chat của hội thoại không còn kết nối nên không gửi được. Quản trị viên shop cần nối lại trang.

### conversation-not-replyable {#conversation-not-replyable}

**422.** Hội thoại này không trả lời được qua API ở trạng thái hiện tại.

---

Nguồn: https://danix.vn/developers/rate-limits.md

# Hạn mức gọi

Hạn mức tính trong cửa sổ một phút, ở ba bộ đếm tách nhau — hai bộ tính **theo từng khoá**, bộ vận đơn tính **theo shop**:

| Bộ đếm | Áp cho | Mặc định |
| --- | --- | --- |
| Chung | Mọi đường của `/api/open/v1`, trừ các đường của hai bộ bên dưới | 600 request mỗi phút mỗi khoá |
| Gửi tin | Gửi tin nhắn (`POST /conversations/:id/messages`) và trả lời riêng bình luận (`POST /comments/:id/private-reply`) | 60 request mỗi phút mỗi khoá |
| Vận đơn | Tạo vận đơn (`POST /shipments`) và huỷ vận đơn (`POST /shipments/:id/cancel`) | 60 request mỗi phút mỗi **shop**, chung cho mọi khoá của shop |

Mỗi request rơi vào đúng một bộ đếm. Request gửi tin không ăn vào 600 của bộ đếm chung, và đọc nhiều không làm cạn hạn mức gửi tin. Tạo và huỷ vận đơn gọi sang hãng vận chuyển, nên trần của chúng tính theo shop: thêm khoá không nâng được trần ấy.

Request mang khoá không hợp lệ (sai định dạng, không tồn tại) được đếm theo địa chỉ IP, và vẫn nhận 401 như thường.

## Header

Mỗi phản hồi có ba header cho biết hạn mức còn lại:

| Header | Ý nghĩa |
| --- | --- |
| `X-RateLimit-Limit` | Trần của bộ đếm áp cho request này. |
| `X-RateLimit-Remaining` | Số request còn lại trong cửa sổ hiện tại. |
| `X-RateLimit-Reset` | Số **giây** còn lại tới khi cửa sổ làm mới (không phải mốc thời gian). |

Vượt hạn mức nhận 429 với `code` là [`rate-limited`](/developers/errors#rate-limited) và header `Retry-After`: số giây phải chờ trước khi gọi lại.

## Cách xử lý

- Chủ động giãn nhịp: đọc `X-RateLimit-Remaining`, và khi gần về 0 thì chờ `X-RateLimit-Reset` giây.
- Gặp 429 thì chờ đúng `Retry-After` rồi thử lại, kèm một chút ngẫu nhiên để nhiều tiến trình không dồn vào cùng một giây.
- Đừng thử lại ngay trong vòng lặp chặt: mỗi lần thử lại cũng bị đếm.
- Khi thử lại một request tạo mới, gửi cùng [Idempotency-Key](/developers/idempotency) để không tạo trùng.
- Đồng bộ lượng lớn: dùng `limit=100` và [`updatedSince`](/developers/pagination) thay vì đọc từng bản ghi.

## Trần của bên thứ ba

Sau API của DANIX còn có trần của Facebook và Zalo khi gửi tin. Chúng áp lên **trang** chứ không lên khoá, nên không vượt được bằng cách chia nhỏ ra nhiều khoá: nhiều khoá cùng gửi qua một trang vẫn dùng chung trần của trang ấy.

---

Nguồn: https://danix.vn/developers/pagination.md

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

---

Nguồn: https://danix.vn/developers/idempotency.md

# Idempotency-Key

`Idempotency-Key` cho phép bạn gọi lại một request tạo mới mà không sợ tạo trùng, ví dụ khi mất kết nối giữa chừng và không biết lượt trước đã thành công hay chưa.

Header **tuỳ chọn**, hỗ trợ ở ba đường:

- `POST /orders` (tạo đơn);
- `POST /customers` (tạo khách);
- `POST /conversations/:id/messages` (gửi tin nhắn).

```bash
curl -X POST "https://danix.vn/api/open/v1/customers" \
  -H "Authorization: Bearer dnx_live_…" \
  -H "Idempotency-Key: a3f9d1c2-7b4e-4c6a-8d21-5e0f9b8c7a64" \
  -H "Content-Type: application/json" \
  -d @customer.json
```

Dùng một chuỗi ngẫu nhiên khó đoán, thường là UUID v4, và sinh **một lần cho mỗi ý định** (một đơn bạn muốn tạo), rồi dùng lại chính nó khi thử lại ý định ấy.

## Hành vi

| Tình huống | Kết quả |
| --- | --- |
| Lần đầu thấy khoá | Thực hiện bình thường và lưu kết quả. |
| Gửi lại cùng khoá, cùng thân, lượt trước đã xong | Phát lại **đúng phản hồi đã lưu**, kèm header `Idempotent-Replayed: true`; không tạo thêm gì. |
| Gửi lại cùng khoá khi lượt trước **còn đang chạy** | 409 [`idempotency-key-in-progress`](/developers/errors#idempotency-key-in-progress) **ngay lập tức**, kèm `Retry-After: 1`. Không chờ. Gửi lại y nguyên sau một giây. |
| Cùng khoá nhưng thân khác | 422 [`idempotency-key-reused`](/developers/errors#idempotency-key-reused). Dùng khoá mới cho nội dung mới. |

## Thời hạn và phạm vi

- Khoá được giữ **24 giờ**. Sau đó, dùng lại cùng chuỗi được coi là một khoá mới và thực hiện lượt mới.
- Khoá có phạm vi theo ứng dụng kết nối: hai ứng dụng khác nhau dùng cùng một chuỗi không đụng nhau.
- Kết quả được ghi cùng giao dịch với lượt tạo, nên không có trường hợp tạo xong mà khoá chưa ghi.
- Chỉ lượt **thành công** được giữ. Lượt bị từ chối (ví dụ 400, 422 thiếu tồn kho) huỷ cả giao dịch lẫn khoá, nên gửi lại cùng khoá sau khi sửa nguyên nhân là chạy lại thật — với cùng thân thì kết quả có thể khác lần trước.

## Với gửi tin nhắn

Gửi tin dùng cơ chế chống trùng sẵn có của chat: DANIX suy ra mã tin nhắn từ shop, ứng dụng, **hội thoại** và `Idempotency-Key`, nên thử lại cùng khoá trong cùng hội thoại không gửi tin hai lần tới khách. Những điểm khác với tạo đơn và tạo khách:

- Phạm vi là **một hội thoại**: cùng khoá gửi vào hai hội thoại khác nhau là hai tin, không phải lỗi `idempotency-key-reused`.
- Gửi lại cùng khoá trả **trạng thái hiện tại** của tin (ví dụ lần đầu `pending`, lần sau đã `sent`), không phải nguyên văn phản hồi lần đầu. Ngoại lệ: khi shop gửi tin qua hàng đợi, một tin `pending` mà hàng đợi bỏ cuộc **không** chuyển sang `failed` — nó biến khỏi `GET /conversations/{id}/messages` sau khoảng 5 phút. Đừng dựa vào trạng thái tin để biết lượt gửi qua hàng đợi đã hỏng.
- Kênh lỗi **tạm thời** (Facebook giới hạn tốc độ gọi của trang, máy chủ của kênh lỗi) trả 503 [`channel-unavailable`](/developers/errors#channel-unavailable): thử lại với **cùng** khoá và **cùng** thân là một lượt gửi mới, không phát lại lượt hỏng. Kênh từ chối **vĩnh viễn** trả 201 với `status` là `failed`, và kết quả ấy được phát lại cho cùng khoá trong 24 giờ — muốn gửi lại thì dùng khoá mới.

Sau 24 giờ, cùng một khoá ra mã tin mới và tin được gửi như một tin mới.

## Khuyến nghị

- Luôn gửi `Idempotency-Key` khi tạo đơn, tạo khách và gửi tin từ hệ thống tự động.
- Khi gặp lỗi mạng, hết giờ hoặc 5xx, thử lại với **cùng** khoá. Khi gặp 4xx khác 409, sửa request thay vì thử lại.

---

Nguồn: https://danix.vn/developers/webhooks.md

# Luồng tự động và webhook

Webhook của DANIX là một **luồng tự động**: một đồ thị các khối nối với nhau mà người dùng vẽ trong ứng dụng Tự động hoá của shop. Khối **Gửi HTTP** là khối gửi dữ liệu ra ngoài, và đó chính là webhook. Trang này nói về phía bên nhận: dữ liệu đến ra sao, cách kiểm chữ ký, thử lại và chống trùng. Cách các khối chạy (thứ tự, mục, Gộp, Gom, Khi lỗi) ở trang [Cách luồng chạy](/developers/flow-logic).

## Luồng trông thế nào

Một luồng gồm các khối **Kích hoạt**, **Nếu**, **Gộp**, **Gom** và **Gửi HTTP**, nối với nhau bằng dây và có thể rẽ nhánh. Luồng chạy **lần lượt**, theo cách của n8n: đi trọn một nhánh tới cuối rồi mới sang nhánh kế.

- Khối **Gửi HTTP** gửi **một lượt cho mỗi mục** nhận vào. Bật "Chạy một lần" thì chỉ mục đầu được gửi. Muốn gửi cả danh sách trong **một** lượt, đặt khối **Gom** phía trước.
- Bên nhận có thể trả JSON. Khối đứng sau dùng được mã trả lời, header và thân đó (xem bên dưới).
- Shop hết hạn gói thì luồng dừng và **không gửi bù** khi gia hạn.

## Một lượt gửi

Mỗi lượt là một request `POST` (hoặc `PUT`, `PATCH` theo cấu hình khối) tới địa chỉ HTTPS bạn khai, với thân JSON. Có hai kiểu thân: **chuẩn** (đúng [envelope sự kiện](/developers/events)) hoặc **mẫu JSON** do người dùng viết, chèn được biến từ sự kiện và từ các khối đứng trước.

Header của request:

| Header | Ý nghĩa |
| --- | --- |
| `Content-Type` | `application/json`. |
| `User-Agent` | `Danix-Webhooks/1.0`. |
| `webhook-id` | Mã của lượt gửi, dạng `msg_…`. Giữ nguyên qua mọi lần thử lại. Dùng để chống trùng. |
| `webhook-timestamp` | Thời điểm ký, số **giây** từ 1970. |
| `webhook-signature` | Chữ ký, dạng `v1,<base64>`. Có thể có nhiều chữ ký cách nhau bằng dấu cách khi bí mật đang được xoay. |

Cộng thêm các header người dùng khai ở khối (tối đa 10; không được trùng tên với các header ở bảng trên, `host`, `content-length`, `transfer-encoding`, `connection`, `upgrade`, `keep-alive` và `expect`).

Thời gian chờ tối đa cho một lượt là 15 giây. Bên nhận nên **trả lời nhanh** (2xx ngay khi đã nhận) rồi xử lý bất đồng bộ.

## Chữ ký (Standard Webhooks)

DANIX ký theo chuẩn [Standard Webhooks](https://www.standardwebhooks.com/), nên các thư viện kiểm chữ ký chính thức của chuẩn dùng được. Mỗi khối Gửi HTTP có **bí mật ký riêng**, dạng `whsec_<base64>`, hiện trong cài đặt của khối (cần quyền quản lý luồng; mỗi lần hiện đều được ghi vết).

Cách tính chữ ký:

1. Nội dung được ký là chuỗi `webhook-id`, dấu chấm, `webhook-timestamp`, dấu chấm, rồi **thân thô** của request.
2. Khoá là phần base64 sau `whsec_`, giải mã ra byte.
3. Chữ ký là HMAC-SHA256 của nội dung với khoá đó, mã hoá base64, ghi `v1,<base64>`.

Khi kiểm chữ ký, nhớ:

- Dùng **thân thô** đúng từng byte đã nhận. Parse JSON rồi serialize lại sẽ làm lệch chữ ký.
- So sánh bằng hàm so sánh thời gian hằng (`timingSafeEqual`, `hmac.compare_digest`, `hash_equals`), không so `==`.
- Kiểm `webhook-timestamp` nằm trong dung sai (mẫu dùng 5 phút) để chặn kẻ gửi lại một request cũ.
- Chấp nhận nếu **bất kỳ** chữ ký `v1` nào trong header khớp.

Xoay bí mật có thời gian chồng (mặc định 24 giờ): trong thời gian đó header mang hai chữ ký, một của bí mật mới và một của bí mật cũ, nên bên nhận đổi bí mật không bị gián đoạn. Lượt gửi "Chạy thử" của một khối **chưa lưu** chưa có bí mật nên được gửi **không có** header chữ ký; mã kiểm bên dưới sẽ từ chối nó, đúng như mong muốn. Chạy thử một bản nháp đã đổi địa chỉ nhận sang một gốc (scheme, tên máy, cổng) khác với bản đã lưu cũng được gửi không chữ ký, để bí mật mà địa chỉ cũ tin không đi tới địa chỉ mới. Lưu luồng rồi chạy thử lại để thử cả phần kiểm chữ ký.

### Node.js (đã chạy với vector kiểm thử)

Mẫu này được kiểm tự động với vector kiểm thử của Svix trong bộ test của chính DANIX.

```js
// Kiểm chữ ký webhook (Standard Webhooks) bằng Node.js, không cần thư viện ngoài.
// Với Express, đọc thân THÔ bằng express.raw({ type: 'application/json' }):
// ký trên đúng các byte đã nhận, không phải JSON đã parse rồi serialize lại.
import { createHmac, timingSafeEqual } from 'node:crypto'

/**
 * @param {string} secret      Bí mật ký của khối, dạng "whsec_...".
 * @param {Record<string, string | undefined>} headers  Header của request, tên chữ thường.
 * @param {string | Buffer} rawBody  Thân thô của request.
 * @param {object} [options]
 * @param {number} [options.toleranceSeconds]  Dung sai timestamp, mặc định 300 giây.
 * @param {number} [options.nowSeconds]        Giờ hiện tại theo giây, chỉ để kiểm thử.
 * @returns {unknown} Thân đã parse, chỉ khi chữ ký hợp lệ.
 */
export function verifyWebhook(secret, headers, rawBody, options = {}) {
  const toleranceSeconds = options.toleranceSeconds ?? 300
  const nowSeconds = options.nowSeconds ?? Math.floor(Date.now() / 1000)

  const id = headers['webhook-id']
  const timestamp = headers['webhook-timestamp']
  const signatures = headers['webhook-signature']
  if (!id || !timestamp || !signatures) throw new Error('Thiếu header chữ ký')

  // Chặn gửi lại một request cũ: timestamp phải nằm trong dung sai.
  if (!Number.isFinite(Number(timestamp)) || Math.abs(nowSeconds - Number(timestamp)) > toleranceSeconds) {
    throw new Error('Timestamp lệch quá xa')
  }

  const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody, 'utf8')
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64')
  const signedContent = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), body])
  const expected = createHmac('sha256', key).update(signedContent).digest()

  // Header có thể mang nhiều chữ ký cách nhau bằng dấu cách (khi đang xoay bí mật).
  for (const part of signatures.split(' ')) {
    const [version, value] = part.split(',')
    const given = version === 'v1' && value ? Buffer.from(value, 'base64') : null
    if (given && given.length === expected.length && timingSafeEqual(given, expected)) {
      return JSON.parse(body.toString('utf8'))
    }
  }

  throw new Error('Chữ ký không hợp lệ')
}
```

Dùng với Express:

```js
import express from 'express'
import { verifyWebhook } from './verify-webhook.mjs'

const app = express()

// express.raw giữ thân thô; không dùng express.json() cho route này.
app.post('/hooks/danix', express.raw({ type: 'application/json' }), (req, res) => {
  let event
  try {
    event = verifyWebhook(process.env.DANIX_WEBHOOK_SECRET, req.headers, req.body)
  } catch {
    return res.sendStatus(400)
  }

  // Xử lý bất đồng bộ; trả 2xx ngay khi đã nhận.
  res.sendStatus(204)
})
```

### Python (mẫu tham khảo)

Viết theo cùng thuật toán với mẫu Node.js; chưa nằm trong bộ test tự động của DANIX.

```python
# Kiểm chữ ký webhook (Standard Webhooks) bằng Python, chỉ dùng thư viện chuẩn.
# Mẫu tham khảo viết theo cùng thuật toán với mẫu Node.js.
import base64
import hashlib
import hmac
import json
import time


def verify_webhook(secret, headers, raw_body, tolerance_seconds=300, now=None):
    """Trả về thân đã parse nếu chữ ký hợp lệ, ngược lại ném ValueError.

    secret    -- bí mật ký của khối, dạng "whsec_..."
    headers   -- dict header của request, tên chữ thường
    raw_body  -- thân THÔ của request (bytes), không phải JSON đã parse
    """
    webhook_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp")
    signatures = headers.get("webhook-signature")
    if not webhook_id or not timestamp or not signatures:
        raise ValueError("Thiếu header chữ ký")

    # Chặn gửi lại một request cũ: timestamp phải nằm trong dung sai.
    current = time.time() if now is None else now
    try:
        sent_at = int(timestamp)
    except ValueError:
        raise ValueError("Timestamp không hợp lệ")
    if abs(current - sent_at) > tolerance_seconds:
        raise ValueError("Timestamp lệch quá xa")

    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed_content = f"{webhook_id}.{timestamp}.".encode() + raw_body
    expected = hmac.new(key, signed_content, hashlib.sha256).digest()

    # Header có thể mang nhiều chữ ký cách nhau bằng dấu cách (khi đang xoay bí mật).
    for part in signatures.split(" "):
        version, _, value = part.partition(",")
        if version != "v1" or not value:
            continue
        if hmac.compare_digest(base64.b64decode(value), expected):
            return json.loads(raw_body)

    raise ValueError("Chữ ký không hợp lệ")
```

### PHP (mẫu tham khảo)

Viết theo cùng thuật toán với mẫu Node.js; chưa nằm trong bộ test tự động của DANIX.

```php
<?php
// Kiểm chữ ký webhook (Standard Webhooks) bằng PHP, không cần thư viện ngoài.
// Mẫu tham khảo viết theo cùng thuật toán với mẫu Node.js.

/**
 * Trả về thân đã parse nếu chữ ký hợp lệ, ngược lại ném Exception.
 *
 * @param string $secret   Bí mật ký của khối, dạng "whsec_...".
 * @param array  $headers  Header của request, tên chữ thường.
 * @param string $rawBody  Thân THÔ của request, ví dụ file_get_contents('php://input').
 */
function verifyWebhook(string $secret, array $headers, string $rawBody, int $toleranceSeconds = 300): array
{
    $id = $headers['webhook-id'] ?? null;
    $timestamp = $headers['webhook-timestamp'] ?? null;
    $signatures = $headers['webhook-signature'] ?? null;
    if (!$id || !$timestamp || !$signatures) {
        throw new Exception('Thiếu header chữ ký');
    }

    // Chặn gửi lại một request cũ: timestamp phải nằm trong dung sai.
    if (!ctype_digit((string) $timestamp) || abs(time() - (int) $timestamp) > $toleranceSeconds) {
        throw new Exception('Timestamp lệch quá xa');
    }

    $key = base64_decode(preg_replace('/^whsec_/', '', $secret), true);
    $expected = hash_hmac('sha256', "{$id}.{$timestamp}.{$rawBody}", $key, true);

    // Header có thể mang nhiều chữ ký cách nhau bằng dấu cách (khi đang xoay bí mật).
    foreach (explode(' ', $signatures) as $part) {
        [$version, $value] = array_pad(explode(',', $part, 2), 2, null);
        if ($version !== 'v1' || !$value) {
            continue;
        }
        $given = base64_decode($value, true);
        if ($given !== false && hash_equals($expected, $given)) {
            return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
        }
    }

    throw new Exception('Chữ ký không hợp lệ');
}
```

## Phản hồi của bên nhận và thử lại

| Bên nhận trả | DANIX làm gì |
| --- | --- |
| Mọi mã **2xx** | Thành công. |
| 3xx | Coi là lỗi (không đi theo chuyển hướng). Hãy khai đúng địa chỉ cuối. |
| 410 | Tạm dừng luồng ngay: bên nhận nói địa chỉ này đã bỏ. |
| 429 hoặc 503 kèm `Retry-After` | Chờ theo `Retry-After`, tối đa 1 giờ, rồi thử lại. |
| Các mã lỗi khác, hết giờ, lỗi mạng | Thử lại theo lịch bên dưới. |

Lịch thử lại có **8 lượt gửi trong khoảng 44 giờ**: lượt đầu, rồi cách 5 giây, 5 phút, 30 phút, 2 giờ, 6 giờ, 12 giờ, 24 giờ, mỗi khoảng có độ lệch ngẫu nhiên khoảng 20% để tránh dồn nhịp. Có thể tắt thử lại trong cài đặt khối. Trong lúc một lượt chờ thử lại, **lượt chạy dừng chờ ở khối ấy**: các mục và nhánh phía sau đợi, đúng thứ tự.

Không thử lại những lỗi mà thử lại cũng không khác đi: địa chỉ bị chặn (địa chỉ nội bộ, địa chỉ của hạ tầng DANIX), URL hỏng, nội dung gửi quá 256 KB, lỗi mẫu JSON.

Luồng **tự tạm dừng** khi một khối Gửi HTTP không có lượt thành công nào trong hơn 72 giờ và đã có ít nhất 10 lỗi liên tiếp **do bên nhận**; lỗi phía DANIX không tính. Luồng bị tắt, việc đang chờ bị huỷ, hành động này được ghi vào nhật ký của shop, và shop bật lại luồng bằng tay khi bên nhận ổn.

## Chống trùng

DANIX giao theo kiểu **ít nhất một lần**: cùng một lượt có thể tới hơn một lần (ví dụ bên nhận đã xử lý xong nhưng phản hồi bị mất, nên lượt đó được thử lại). Hãy lưu `webhook-id` đã xử lý và bỏ qua lượt trùng. `webhook-id` được suy từ lần gọi khối và vị trí của mục, nên **không đổi** qua mọi lần thử lại và qua nút "Gửi lại với bản cũ". Riêng "Gửi lại với bản luồng đang lưu" là tin mới nên mang `webhook-id` mới. Thứ tự các lượt **không được đảm bảo**.

Phân biệt hai mã: `id` trong envelope là mã của **sự kiện** (cùng giá trị ở mọi luồng nhận cùng một sự kiện, dạng `evt_…`), còn `webhook-id` là mã của **lượt gửi** (dạng `msg_…`). Cả hai là chuỗi mờ đục, không phải UUID: lưu vào cột chuỗi.

## Dùng phản hồi của bên nhận trong khối sau

Nếu bên nhận trả JSON, các khối đứng sau khối Gửi HTTP dùng được mã trả lời (`status`), header và thân (`body`) của nó, ví dụ biến `#{NODE(http_1).item.body.id}`. Thân chỉ được lưu **tối đa 64 KB**; thân dài hơn bị cắt. Nếu thân là một mảng thì mỗi phần tử trở thành một mục riêng. Vì vậy **bên nhận đừng trả bí mật trong thân**: phản hồi được lưu để hiện trong nhật ký chạy. Header `set-cookie` luôn bị bỏ.

## Nhật ký và gửi lại

Mỗi lượt chạy có nhật ký theo từng khối trong 7 ngày: trạng thái, số mục, từng lần gửi. Lần gửi lỗi, và mọi lần gửi của lượt "Chạy thử", lưu thêm tối đa 16 KB đầu của thân đã gửi; giá trị header không bao giờ được lưu. Khối lỗi có nút **Gửi lại** (xem [Cách luồng chạy](/developers/flow-logic#gui-lai)). Sau 7 ngày nhật ký bị xoá.

## Dữ liệu khách và yêu cầu xoá

Khi khách chat yêu cầu xoá dữ liệu qua Facebook, DANIX xoá nhật ký chạy có dữ liệu của khách ấy. Bản đã gửi tới hệ thống của bạn thì **bạn tự xoá** theo nghĩa vụ của mình.

## Địa chỉ nhận và an toàn

- Địa chỉ phải là **HTTPS**. DANIX từ chối địa chỉ trỏ vào mạng nội bộ và vào hạ tầng của chính DANIX.
- Địa chỉ và header **không nhận biến**: dữ liệu từ bên ngoài không bao giờ quyết được nơi dữ liệu được gửi tới.
- Đừng đặt khoá hay token trong địa chỉ nhận nếu tránh được. Người chỉ có quyền **xem** luồng thấy địa chỉ đã che (chỉ còn phần gốc), nhưng khoá vẫn nằm trong cấu hình luồng. Dùng header bí mật hoặc chữ ký thay cho khoá trong URL.
- Header khai là "bí mật" chỉ được gửi tới đúng địa chỉ mà nó được khai cùng. Đổi địa chỉ thì header bí mật bị xoá và phải nhập lại.

## IP gửi đi

Các lượt gửi của luồng tới từ địa chỉ IP **103.179.173.155**. Nếu bên nhận chặn theo IP, hãy cho phép địa chỉ này. Địa chỉ có thể đổi khi DANIX chuyển máy chủ; thay đổi sẽ được báo trước, nhưng không nên coi đây là cách xác thực duy nhất: **luôn kiểm chữ ký**.

---

Nguồn: https://danix.vn/developers/flow-logic.md

# Cách luồng chạy (cho người quen n8n)

Luồng tự động của DANIX chạy theo logic của **n8n**, để người đã dùng n8n không phải học lại. Trang này đặt tên khái niệm n8n ở lần nhắc đầu (Merge, Aggregate, On Error, Execute Once, paired item, Retry with currently saved / original workflow) rồi dùng tên tiếng Việt như trên màn hình của ứng dụng Tự động hoá. Cuối trang có bảng [Khác n8n](#khac-n8n).

## Tóm tắt Giống n8n / Khác n8n

| Giống n8n | Khác n8n |
| --- | --- |
| Chạy lần lượt từng nhánh, nhánh nằm trên chạy trước | Chỉ MỘT khối Kích hoạt mỗi luồng, đồ thị không có vòng |
| Mỗi khối nhận và ra một danh sách **mục** | Không có khối mã (Code) và không có kiểu gộp SQL |
| Khối Gửi HTTP chạy cho từng mục | Địa chỉ nhận và header không nhận biến |
| Khối Gộp (Merge) và khối Gom (Aggregate) | Kiểu Gom "cả mục" luôn đặt danh sách vào trường `items` |
| "Khi lỗi" ba lựa chọn (ở khối Gửi HTTP), "Chạy một lần" như Execute Once | "Khi lỗi" chỉ có ở khối Gửi HTTP; lưu luồng huỷ cả lượt đang chạy của bản cũ |
| "Gửi lại" hai nút | Thử lại dài tới khoảng 44 giờ do DANIX lo |

## 1. Thứ tự chạy

Thứ tự giống thứ tự v1 của n8n: luồng đi **trọn một nhánh tới cuối** rồi mới sang nhánh kế. Khi một khối có nhiều khối con, khối con nằm **trên** (toạ độ y nhỏ hơn) chạy trước; cùng độ cao thì khối nằm bên **trái** chạy trước. Số thứ tự hiện ngay trên mỗi khối ở bảng vẽ, nên bạn kéo khối lên hay xuống để đổi thứ tự.

Ví dụ: khối Kích hoạt nối vào khối Nếu. Ngả **Đúng** nối tới khối A (ở trên), A nối tiếp tới khối C. Ngả **Sai** nối tới khối B (ở dưới). Thứ tự là Kích hoạt (1), Nếu (2), A (3), C (4), B (5): nhánh trên chạy trọn tới C rồi mới sang B. Số thứ tự trên bảng vẽ giả định mọi cổng đều có mục; lượt chạy thật có thể bỏ qua khối không nhận được mục nào.

Một **lượt chạy** tại một thời điểm chỉ chạy **một khối**. Vì thế không có tranh chấp giữa hai nhánh của cùng lượt về dữ liệu. Nhưng nhiều lượt chạy của cùng một luồng (mỗi sự kiện một lượt) có thể chạy song song, nên bên nhận có thể nhận nhiều request cùng lúc từ một luồng — đừng giả định luồng gửi tuần tự.

## 2. Mục (item)

Mỗi khối nhận vào và đưa ra một **danh sách mục**. Khối Kích hoạt ra đúng một mục: envelope của sự kiện. Khối **Gửi HTTP** chạy **một lượt cho mỗi mục** và ra một mục `{ status, headers, body }` cho mỗi lượt (thân trả về là mảng thì mỗi phần tử thành một mục).

Để lấy dữ liệu của khối trước, dùng biến `#{NODE(<khoá>).…}` trong mẫu JSON và trong bộ lọc, với `<khoá>` là khoá ngắn của khối (hiện ở cài đặt khối, không đổi khi bạn đổi nhãn):

| Biến | Nghĩa | Tương đương n8n |
| --- | --- | --- |
| `#{NODE(<khoá>).item.<đường>}` | Mục của khối ấy **gắn với mục đang xử lý** (như paired item). Dạng chính. | `$('Khối').item` |
| `#{NODE(<khoá>).first.<đường>}` | Mục đầu tiên khối ấy ra. | `$('Khối').first()` |
| `#{NODE(<khoá>).count}` | Số mục khối ấy ra (không tính cổng lỗi). | `$('Khối').all().length` |
| `#{NODE(<khoá>).items}` | Danh sách mọi mục (`json`) của khối ấy, thành mảng. Chỉ đứng **trọn một giá trị chuỗi** của mẫu, không dùng trong bộ lọc. | `$('Khối').all()` |

Ví dụ: sau một khối Gửi HTTP có khoá `http_1`, viết `#{NODE(http_1).item.body.id}` hay `#{NODE(http_1).item.status}`. Đường dẫn sau `.item` hay `.first` gồm các đoạn chữ, số, `_`, `-`, tối đa 8 đoạn; chỉ số mảng viết bằng số.

Hai điều khác biểu thức n8n: không có đoạn `.json` (n8n viết `$('Khối').item.json.body.id`, DANIX viết `#{NODE(http_1).item.body.id}`), và biến chỉ **thay giá trị** trên cây JSON chứ không chạy biểu thức JavaScript.

Quy tắc khác:

- Biến chỉ tham chiếu được khối **tổ tiên**: khối nằm trên đường dây dẫn tới khối đang soạn.
- Khối chưa chạy ở lượt này cho giá trị vắng: biến chiếm trọn một giá trị chuỗi thành `null`, biến lẫn trong chuỗi thành rỗng, `.count` thành 0.
- Khối có **nhiều dây vào** chạy một lần cho **mỗi nhánh** tới, như n8n.
- Khối nhận **0 mục** thì không chạy: nhật ký không có lần gọi nào của nó.

## 3. Gộp (Merge)

Khối **Gộp** nhận 2 đến 4 danh sách ở các cổng `in1`…`in4`, **chờ đủ mọi đầu vào** rồi mới chạy, và ra một danh sách. Mỗi cổng vào phải có đúng một dây. Nếu một nhánh không bao giờ tới (ví dụ nhánh Nếu bị bỏ) thì khi không còn việc nào khác, Gộp chạy với các đầu vào đang có. Có năm kiểu, không có kiểu SQL (nó cho chạy truy vấn tuỳ ý trên máy chủ).

Ghép hai mục là ghép nông theo khoá của `json`; trùng khoá thì cổng đứng sau thắng, như n8n ưu tiên Input 2. Số mục ra vượt 100 thì lượt chạy thất bại với `limit_exceeded`, không cắt âm thầm.

### Nối danh sách {#append}

Kiểu `append` (Append của n8n). Mọi mục của cổng 1, rồi cổng 2…, giữ thứ tự. Vào: cổng 1 `[{a:1}, {a:2}]`, cổng 2 `[{a:3}]`. Ra: `[{a:1}, {a:2}, {a:3}]`.

### Ghép theo trường trùng {#combine_by_fields}

Kiểu `combine_by_fields` (Combine by Matching Fields). Ghép cặp mục của hai cổng có giá trị trùng ở 1 đến 3 cặp trường (`left` ở cổng 1, `right` ở cổng 2). Chỉ dùng với **hai cổng vào**. Tuỳ chọn `keep`:

- `matches` (mặc định): chỉ giữ cặp khớp;
- `input1_enriched`: giữ mọi mục của cổng 1 và bổ sung dữ liệu từ mục khớp ở cổng 2;
- `all`: giữ cả cặp khớp lẫn mục không khớp của hai cổng.

Ví dụ ghép đơn với khách theo `customerId` = `id`: vào cổng 1 `[{orderCode:"A1", customerId:7}]`, cổng 2 `[{id:7, name:"Lan"}]`, ra `[{orderCode:"A1", customerId:7, id:7, name:"Lan"}]`. Trường so sánh theo giá trị: chuỗi theo chuỗi, số theo số.

### Ghép theo thứ tự {#combine_by_position}

Kiểu `combine_by_position` (Combine by Position). Mục thứ i của mọi cổng ghép thành một mục. Số mục ra bằng số mục của cổng **ít nhất**. Vào: cổng 1 `[{a:1}, {a:2}]`, cổng 2 `[{b:"x"}]`. Ra: `[{a:1, b:"x"}]`.

### Mọi tổ hợp {#all_combinations}

Kiểu `all_combinations` (All Possible Combinations). Mọi cặp mục cổng 1 × cổng 2; chỉ dùng với **hai cổng vào**. Vào: cổng 1 `[{a:1}, {a:2}]`, cổng 2 `[{b:"x"}, {b:"y"}]`. Ra 4 mục: `{a:1,b:"x"}`, `{a:1,b:"y"}`, `{a:2,b:"x"}`, `{a:2,b:"y"}`.

### Chọn một ngả {#choose_branch}

Kiểu `choose_branch` (Choose Branch). Đợi mọi cổng, rồi ra nguyên các mục của **cổng đã chọn** (`input`), bỏ các cổng còn lại. Dùng để đồng bộ hai nhánh mà chỉ lấy dữ liệu của một nhánh.

## 4. Gom (Aggregate)

Khối **Gom** nhận **mọi mục** đầu vào và gộp thành **một mục** ra. Dùng khi muốn gửi cả danh sách trong **một** lượt: đặt Gom ngay trước khối Gửi HTTP. Tối đa 100 mục vào; vượt thì `limit_exceeded`.

### Gom cả mục vào một danh sách {#all_items}

Kiểu `all_items`. Ra một mục `{ items: [<json mục 1>, <json mục 2>, …] }`. Vào ba mục `{id:101}`, `{id:102}`, `{id:103}`; ra một mục `{ items: [{id:101},{id:102},{id:103}] }`. Khác n8n: tên trường cố định là `items` (n8n mặc định `data` và cho đổi), để biến và tài liệu chỉ có một tên.

### Gom từng trường thành danh sách {#fields}

Kiểu `fields`. Mỗi trường bạn chọn (`path` và tên ra `as`) thành một danh sách giá trị. Với `fields: [{ path: "body.id", as: "ids" }]`, vào ba mục có `body.id` là 101, 102, 103, ra một mục `{ ids: [101, 102, 103] }`. Mục thiếu trường ấy thì bỏ qua giá trị ấy. Tối đa 10 trường; tên ra khớp `^[A-Za-z_][A-Za-z0-9_]{0,63}$` và không dùng `__proto__`, `constructor`, `prototype`.

Sau khối Gom, `#{NODE(<khối trước Gom>).item…}` **không còn xác định** (không biết chọn mục nào trong số đã gom); hãy dùng `#{NODE(gom).item.items}`, `.first`, `.count` hoặc `.items` của chính khối Gom.

## 5. Khi lỗi (On Error)

Chỉ khối **Gửi HTTP** có cài đặt **Khi lỗi**, với ba lựa chọn. Khối Nếu, Gộp, Gom chỉ lỗi khi vượt giới hạn số mục hay dung lượng, và vượt giới hạn luôn dừng cả lượt chạy (`limit_exceeded`), nên ba khối này không có cài đặt ấy: `onError` khác `stop` gửi lên ở chúng được đưa về `stop`. Khối Kích hoạt cũng không có, và luôn dừng khi lỗi. Với khối Gửi HTTP, "lỗi" là lỗi **sau khi đã hết lịch thử lại** (hoặc ngay lượt đầu nếu bạn tắt thử lại, hoặc lỗi không thử lại được).

### Dừng luồng {#stop}

Giá trị `stop` (mặc định). Lần gọi khối ghi `failed`, cả lượt chạy `failed`, các khối đang chờ phía sau bị huỷ với lý do `stopped_by_error` nhưng được giữ lại để bạn dùng [Gửi lại](#gui-lai) chạy tiếp.

### Bỏ qua và chạy tiếp {#continue}

Giá trị `continue` (Continue của n8n). Mục lỗi trở thành mục `{ error: { code, message, status? } }` đi ra **cổng thường**, nên các khối phía sau vẫn chạy và có thể đọc `error`.

### Đi theo ngả lỗi {#error_port}

Giá trị `error_port` (Continue using error output). Khối Gửi HTTP có thêm cổng ra `error`: mục lỗi ra cổng `error`, mục thành công ra cổng `out`. Bắt buộc nối dây từ cổng `error` (để mục lỗi không rơi mất mà không ai thấy), và ngược lại cổng `error` chỉ có dây khi chọn lựa chọn này.

## Chạy một lần (Execute Once)

Cài đặt **Chạy một lần (chỉ mục đầu)** giống Execute Once của n8n: khối chỉ nhận **mục đầu** của mỗi cổng vào, các mục còn lại bị bỏ. Khối Kích hoạt không có cài đặt này. Muốn gửi cả danh sách trong một lượt, dùng khối Gom thay vì Chạy một lần.

## Thử lại

Khối Gửi HTTP thử lại dài **8 lượt trong khoảng 44 giờ** (bật mặc định, tắt được), do DANIX lo. Trong lúc chờ, lượt chạy **dừng ở khối ấy**. Lịch và quy tắc: [Luồng tự động và webhook](/developers/webhooks#phan-hoi-cua-ben-nhan-va-thu-lai). Lỗi phía DANIX (ví dụ máy chủ đang khởi động lại giữa một lượt gửi) được thử lại riêng theo lịch ngắn — 5 giây, 30 giây, 2 phút — ở luồng đang chạy thật, kể cả khi bạn tắt thử lại; lượt "Chạy thử" không thử lại.

## Gửi lại

Khối lỗi trong nhật ký có hai nút, như n8n (Retry with currently saved workflow / Retry with original workflow). Cả hai chạy lại khối lỗi **và phần phía sau**:

- **Với bản luồng đang lưu**: dùng địa chỉ, thông tin bí mật và quyền của bản hiện tại. Đây là tin mới nên mang `webhook-id` **mới**. Nút mờ khi khối không còn tồn tại (cùng `id` và khoá) hoặc khối Kích hoạt đã đổi.
- **Với bản cũ**: chạy bằng phiên bản của lượt chạy, giữ **nguyên** `webhook-id`. Chỉ dùng được khi địa chỉ nhận của **mọi** khối Gửi HTTP sẽ chạy lại (khối lỗi và các khối phía sau nó) chưa đổi so với bản đã lưu bí mật; một khối trong số ấy đã bị gỡ khỏi bản đang lưu thì nút này bị chặn.

Cả hai kiểm quyền và trang của người bấm, chỉ cho một lượt gửi lại mỗi lúc, và bị từ chối khi luồng đang tắt hoặc shop không dùng được. Gửi lại chỉ có trên **khối lỗi**.

## Khác n8n {#khac-n8n}

- Không có kiểu gộp SQL, và không có khối mã (Code).
- Kiểu Gom "cả mục" luôn đặt danh sách vào trường `items` (n8n mặc định `data`, cho đổi).
- "Khi lỗi" chỉ có ở khối **Gửi HTTP** (n8n có ở mọi khối): khối Nếu, Gộp, Gom lỗi là dừng cả lượt chạy, và một dây cũ còn nối ở cổng `error` của ba khối ấy bị bỏ qua. "Chạy một lần" không có ở khối Kích hoạt.
- Thử lại dài tới khoảng 44 giờ do DANIX lo; n8n không có.
- Mỗi lần gọi khối tối đa **100 mục**, mỗi lượt chạy tối đa **500 lượt xử lý mục**. Vượt thì lượt chạy thất bại `limit_exceeded` (lỗi phía DANIX).
- Địa chỉ nhận và header **không nhận biến** (n8n cho biểu thức ở URL và header).
- Biến chỉ thay giá trị, không chạy biểu thức JavaScript.
- Biến chỉ tham chiếu được khối **tổ tiên**.
- Đúng **một** khối Kích hoạt mỗi luồng và đồ thị **không có vòng** (n8n cho nhiều trigger và có vòng lặp).
- Trần tĩnh của một luồng: 30 khối, 60 dây, 10 khối Gửi HTTP, 10 khối Gộp, 10 khối Gom, đường dài nhất 15 khối.
- Đường biến không có đoạn `.json`.
- **Lưu luồng** (tạo phiên bản mới) **huỷ** cả lượt đang chạy lẫn lượt đang chờ thử lại tới 44 giờ của phiên bản cũ; n8n để chúng chạy nốt.

---

Nguồn: https://danix.vn/developers/events.md

# Sự kiện

Sự kiện là thứ **khởi động** một luồng: khối Kích hoạt chọn một loại sự kiện, và mỗi lần sự kiện ấy xảy ra ở shop, luồng chạy với một mục là **envelope** của sự kiện. DANIX chỉ ghi sự kiện khi shop có luồng đang bật nghe loại ấy (và với tin nhắn, nghe đúng trang ấy) và shop còn dùng được tính năng Tự động hoá.

## Envelope

```json
{
  "id": "evt_018f3b8e-…",
  "type": "order.status_changed",
  "apiVersion": "v1",
  "createdAt": "2026-10-01T08:30:00.000Z",
  "shop": { "id": "…", "slug": "ten-shop" },
  "changes": { "fields": ["status"], "status": { "from": "new", "to": "confirmed" } },
  "data": { "id": "…", "code": "…", "status": "confirmed" }
}
```

| Trường | Ý nghĩa |
| --- | --- |
| `id` | Mã của **sự kiện**: chuỗi có tiền tố `evt_`, tối đa 68 ký tự. Cùng một giá trị ở mọi luồng nhận cùng sự kiện. Khác với `webhook-id` (mã của lượt gửi). Coi nó là chuỗi mờ đục — **không phải UUID**, đừng lưu vào cột kiểu `uuid`. |
| `type` | Loại sự kiện, xem bảng bên dưới. |
| `apiVersion` | Phiên bản hình dạng dữ liệu, hiện là `v1`. |
| `createdAt` | Thời điểm sự kiện xảy ra, ISO 8601 UTC. |
| `shop` | Shop phát sinh sự kiện: `id` và `slug`. |
| `changes` | Chỉ có ở các sự kiện sửa. Xem bên dưới. |
| `data` | Nội dung của tài nguyên, **cùng hình dạng với phản hồi `GET` tương ứng** trong [tham chiếu](/developers/reference/orders): bạn đọc `data` y như đã đọc một bản ghi qua API. |

### `changes`

`changes` cho biết **trường nào đã đổi**, không chứa giá trị cũ của chúng:

- `fields`: danh sách tên trường đã đổi — đúng tên trường của `data` (camelCase, ví dụ `shippingFee`; trường của mẫu mã viết `variants.price`). Đổi liên hệ hay địa chỉ của khách hiện là `contacts` / `addresses`, đổi ảnh sản phẩm là `images`, mốc mới của chặng hoàn (vận đơn giao một phần) là `events`. Thay đổi ở phần dữ liệu không có trong `data` không được liệt kê, và lượt sửa chỉ chạm phần ấy (ví dụ đường dẫn rút gọn của sản phẩm, ảnh đại diện của khách, ghi chú in của đơn) **không được gửi**: DANIX không gửi gì cho bạn, luồng dừng ngay ở khối Kích hoạt (nhật ký của shop vẫn ghi một lượt chạy bị bỏ qua, ẩn mặc định);
- `status`: có khi trạng thái đổi, dạng `{ "from": …, "to": … }`. Đây là giá trị duy nhất ngoài tên trường mà `changes` mang.

Muốn biết giá trị mới của một trường, đọc `data`. Muốn biết giá trị cũ, bạn tự giữ bản sao ở phía mình.

## Danh mục sự kiện

| `type` | Khi nào | `data` là | Quyền đọc cần có |
| --- | --- | --- | --- |
| `order.created` | Đơn hàng được tạo | Đơn hàng | `pos.orders.read` |
| `order.updated` | Một trường trong `data` của đơn được sửa | Đơn hàng | `pos.orders.read` |
| `order.status_changed` | Đơn đổi trạng thái | Đơn hàng | `pos.orders.read` |
| `order.deleted` | Đơn bị xoá | Đơn hàng | `pos.orders.read` |
| `product.created` | Sản phẩm được tạo | Sản phẩm | `pos.products.read` |
| `product.updated` | Một trường trong `data` của sản phẩm được sửa (kể cả mẫu mã, ảnh) | Sản phẩm | `pos.products.read` |
| `product.deleted` | Sản phẩm bị xoá | Sản phẩm | `pos.products.read` |
| `inventory.stock_changed` | Tồn kho của một mẫu mã đổi | Dòng tồn kho | `pos.inventory.read` và `pos.products.read` |
| `customer.created` | Khách hàng được tạo | Khách hàng | `pos.customers.read` |
| `customer.updated` | Một trường trong `data` của khách được sửa (kể cả liên hệ, địa chỉ) | Khách hàng | `pos.customers.read` |
| `customer.deleted` | Khách hàng bị xoá | Khách hàng | `pos.customers.read` |
| `shipment.created` | Vận đơn được tạo | Vận đơn | `pos.shipping.read` và `pos.orders.read` |
| `shipment.status_changed` | Vận đơn đổi `status`, hoặc chặng hoàn của đơn giao một phần sang bước mới | Vận đơn | `pos.shipping.read` và `pos.orders.read` |
| `message.received` | Khách nhắn tin tới một trang chat | Tin nhắn | `social.pages.read` và `social.conversations.read` |
| `message.sent` | Shop gửi tin tới khách | Tin nhắn | `social.pages.read` và `social.conversations.read` |

Luồng chạy dưới một tài khoản riêng của chính nó; **chỉ gửi đi dữ liệu mà tài khoản ấy đọc được** theo cột "Quyền đọc cần có", và với tin nhắn chỉ trên các trang đã chọn cho luồng. Người chỉ có quyền xem luồng không đọc được nội dung gửi của loại dữ liệu mà họ không có quyền đọc.

## Ví dụ từng loại

Các ví dụ dưới đây rút gọn `data` thành `id` kèm vài trường; `data` thật đầy đủ như phản hồi `GET` tương ứng. Nút **Chạy thử** trong ứng dụng Tự động hoá gửi envelope mẫu đầy đủ của từng loại tới khối của bạn.

### Đơn hàng

```json
{ "id": "evt_018f3b8e-…", "type": "order.created", "apiVersion": "v1", "createdAt": "2026-10-01T08:30:00.000Z",
  "shop": { "id": "…", "slug": "ten-shop" },
  "data": { "id": "…", "code": "DH0001", "status": "new" } }
```

```json
{ "id": "evt_018f3b8e-…", "type": "order.updated", "apiVersion": "v1", "createdAt": "2026-10-01T08:35:00.000Z",
  "shop": { "id": "…", "slug": "ten-shop" },
  "changes": { "fields": ["note", "shippingFee"] },
  "data": { "id": "…", "code": "DH0001", "status": "new" } }
```

```json
{ "id": "evt_018f3b8e-…", "type": "order.status_changed", "apiVersion": "v1", "createdAt": "2026-10-01T09:00:00.000Z",
  "shop": { "id": "…", "slug": "ten-shop" },
  "changes": { "fields": ["status"], "status": { "from": "new", "to": "confirmed" } },
  "data": { "id": "…", "code": "DH0001", "status": "confirmed" } }
```

`order.deleted` có cùng hình dạng, `data` là bản ghi đơn đã bị xoá: `status` là `deleted` và có `deletedAt`. `product.deleted` và `customer.deleted` cũng mang `deletedAt`.

### Sản phẩm, khách hàng

`product.created`, `product.updated`, `product.deleted` và `customer.created`, `customer.updated`, `customer.deleted` cùng dạng envelope với đơn hàng: `data` là sản phẩm hay khách hàng; các sự kiện `updated` mang `changes.fields`.

```json
{ "id": "evt_018f3b8e-…", "type": "customer.updated", "apiVersion": "v1", "createdAt": "2026-10-01T10:00:00.000Z",
  "shop": { "id": "…", "slug": "ten-shop" },
  "changes": { "fields": ["contacts"] },
  "data": { "id": "…" } }
```

### Tồn kho

```json
{ "id": "evt_018f3b8e-…", "type": "inventory.stock_changed", "apiVersion": "v1", "createdAt": "2026-10-01T11:00:00.000Z",
  "shop": { "id": "…", "slug": "ten-shop" },
  "changes": { "fields": ["quantity"] },
  "data": { "…": "dòng tồn kho của một mẫu mã tại một kho" } }
```

### Vận đơn

```json
{ "id": "evt_018f3b8e-…", "type": "shipment.status_changed", "apiVersion": "v1", "createdAt": "2026-10-01T12:00:00.000Z",
  "shop": { "id": "…", "slug": "ten-shop" },
  "changes": { "fields": ["status"], "status": { "from": "…", "to": "…" } },
  "data": { "id": "…" } }
```

`shipment.created` cùng dạng, không có `changes`.

Đơn giao một phần: chặng đi kết thúc ở `partially_returned` (trạng thái cuối, không đổi nữa), rồi phần hàng hoàn về theo một mã riêng của hãng trên **cùng** vận đơn. Mỗi bước của chặng hoàn phát một `shipment.status_changed` không có `changes.status` (vì `status` đứng yên), `changes.fields` là `["events"]`, và mốc mới nằm trong `data.events`:

```json
{ "id": "evt_018f3b8e-…", "type": "shipment.status_changed", "apiVersion": "v1", "createdAt": "2026-10-03T09:00:00.000Z",
  "shop": { "id": "…", "slug": "ten-shop" },
  "changes": { "fields": ["events"] },
  "data": { "id": "…", "status": "partially_returned", "events": ["…"] } }
```

### Tin nhắn

```json
{ "id": "evt_018f3b8e-…", "type": "message.received", "apiVersion": "v1", "createdAt": "2026-10-01T13:00:00.000Z",
  "shop": { "id": "…", "slug": "ten-shop" },
  "data": { "id": "…" } }
```

`message.sent` cùng dạng. Khi khối Kích hoạt chọn một trong hai sự kiện tin nhắn, bạn phải chọn **trang** chat mà luồng lắng nghe.

## Ghi chú cho bên nhận

- Danh mục sự kiện và trường trong `data` có thể được **bổ sung** mà không đổi phiên bản: bỏ qua loại sự kiện và trường lạ.
- Sự kiện giao theo kiểu ít nhất một lần và không đảm bảo thứ tự: khử trùng theo `webhook-id` (hoặc theo `id` của sự kiện nếu bạn nhận ở nhiều luồng), và dùng `createdAt` của **envelope** để biết sự kiện nào mới hơn — không phải mọi `data` đều có `updatedAt` (sản phẩm, chi tiết vận đơn và dòng tồn không có). Xem [Luồng tự động và webhook](/developers/webhooks).
- Tạo mới chỉ phát `*.created`: tạo đơn không kèm `order.status_changed`, và liên hệ, địa chỉ, mẫu mã, ảnh tạo cùng lúc với khách hay sản phẩm không kèm `*.updated`.
- Đơn "trả để đổi" (`data.exchangeReturn` là `true`) là đơn khách gửi hàng về để ĐỔI, không phải để hoàn tiền. Lượt duyệt hoàn đổi mục đích của đơn (hoàn ↔ đổi) gửi `order.updated` với `changes.fields` chứa `exchangeReturn`, kể cả khi trạng thái và tiền không đổi.
- `*.updated` chỉ được gửi khi có ít nhất một trường của `data` đổi, nên luôn mang `changes`; sửa phần dữ liệu không có trong `data` không gửi gì. `shipment.status_changed` được gửi khi `status` đổi (mang `changes.status`) và khi chặng hoàn của đơn giao một phần sang bước mới (`changes.fields` là `["events"]`, không có `changes.status`). Mốc hành trình khác không đổi `status` không phát sự kiện; đọc chúng ở `data.events` khi gọi `GET`.
- Webhook là đường thời gian thực; [`updatedSince`](/developers/pagination) là đường đối soát bù những gì bạn có thể đã lỡ.

---

Nguồn: https://danix.vn/developers/versioning.md

# Phiên bản

Phiên bản của API nằm trong đường dẫn: mọi endpoint hiện nay dưới `/api/open/v1`. Envelope sự kiện mang `apiVersion: "v1"` cho cùng ý nghĩa.

## Thay đổi không phá vỡ

Những thay đổi sau có thể xảy ra **mà không đổi phiên bản**, nên client phải chịu được chúng:

- thêm endpoint mới;
- thêm trường mới vào phản hồi, vào `data` của sự kiện hay vào thân lỗi;
- thêm tham số tuỳ chọn mới (query, header, trường thân);
- thêm giá trị mới cho một danh sách giá trị (enum), ví dụ một trạng thái đơn mới hay một loại sự kiện mới;
- thêm mã lỗi `code` mới;
- đổi câu chữ của `title` và `detail` trong lỗi, thứ tự các trường JSON, độ dài của các mã định danh.

## Thay đổi phá vỡ

Những thay đổi sau chỉ xảy ra ở một phiên bản mới (`/v2`), không bao giờ ở `/v1`:

- bỏ hoặc đổi tên một trường, một endpoint hay một tham số;
- đổi kiểu hay ý nghĩa của một trường;
- thêm tham số **bắt buộc**;
- bỏ một giá trị khỏi danh sách giá trị;
- thêm luật kiểm tra mới làm một request đang hợp lệ thành không hợp lệ;
- đổi cách xác thực.

Ngoại lệ trước ngày DANIX mở cho người dùng: `v1` chưa đóng băng — xem mục [Về hỗ trợ và thời hạn](#ve-ho-tro-va-thoi-han) cuối trang.

## Viết client chịu được thay đổi

1. **Bỏ qua trường lạ.** Parse JSON theo kiểu "lấy các trường mình cần", không từ chối khi gặp trường chưa biết.
2. **Bỏ qua giá trị enum lạ.** Gặp trạng thái hay loại sự kiện chưa biết thì lưu lại hoặc bỏ qua, đừng báo lỗi hay dừng cả tiến trình.
3. **Rẽ nhánh theo `code` của lỗi**, không theo `title` hay `detail`; coi `code` lạ như lỗi cùng nhóm `status`.
4. **Không phụ thuộc thứ tự trường** và không phụ thuộc độ dài mã định danh.
5. **Giá trị tiền và số lượng là chuỗi thập phân**: giữ nguyên dạng chuỗi hoặc dùng kiểu số thập phân chính xác, đừng ép sang số dấu phẩy động.
6. Thời gian luôn là ISO 8601 UTC.

## Về hỗ trợ và thời hạn

Tài liệu này mô tả `v1` hiện hành. DANIX chưa công bố cam kết về thời hạn hỗ trợ cho các phiên bản, và chưa phát tín hiệu báo ngừng hỗ trợ trong header phản hồi; khi có, thông tin sẽ được ghi ở trang này. Tệp [openapi.json](/openapi.json) luôn là đặc tả của phiên bản đang chạy.

Trước ngày DANIX mở cho người dùng, `v1` chưa đóng băng và có thể nhận thay đổi phá vỡ. Thay đổi đã có: `POST /orders/{id}/status` từ chối đưa một đơn đã rời `new` quay về `new` (`order-status-not-allowed`).

---

Nguồn: https://danix.vn/developers/reference/shop.md

# Tham chiếu: Shop

Thông tin shop mà khoá API đang thuộc về. Mọi đường dẫn dưới `https://danix.vn/api/open/v1`. Đặc tả máy đọc: [openapi.json](/openapi.json).

## Thông tin shop của khoá {#getShop}

`GET /shop`

Trả shop mà khoá API thuộc về, tên ứng dụng kết nối, các quyền được cấp, các trang chat được dùng và cờ chỉ đọc. Đây là cách nhanh nhất để kiểm khoá hoạt động.

**Quyền cần có:** không cần quyền nào ngoài khoá hợp lệ.

**Ví dụ yêu cầu**

```bash
curl -X GET "https://danix.vn/api/open/v1/shop" \
  -H "Authorization: Bearer dnx_live_…"
```

**Phản hồi mẫu (200)**

```json
{
  "id": "018f3b8e-0000-7000-8000-00000000a001",
  "slug": "shop-mau",
  "name": "Shop Mẫu",
  "application": {
    "name": "Đồng bộ kế toán",
    "permissions": [
      "pos.orders.read"
    ],
    "pageIds": []
  },
  "readOnly": false
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh của shop. |
| `slug` | string | có | Tên ngắn của shop trong đường dẫn. |
| `name` | string | có | Tên shop. |
| `application` | object | có | Thông tin ứng dụng kết nối gắn với khoá. |
| `application.name` | string | có | Tên ứng dụng kết nối đang gọi. |
| `application.permissions` | array<string> | có | Các quyền khoá này được cấp, ví dụ `pos.orders.read`. |
| `application.pageIds` | array<string (uuid)> | có | Mã (UUID) trong DANIX của các trang chat khoá này được dùng — cùng giá trị `id` của `GET /pages`. Rỗng nếu khoá không có quyền chat. |
| `readOnly` | boolean | có | `true` khi gói cước hết hạn: khoá chỉ đọc, ghi trả 402. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

---

Nguồn: https://danix.vn/developers/reference/orders.md

# Tham chiếu: Đơn hàng

Danh sách, chi tiết, tạo, sửa, đổi trạng thái và ghi chú của đơn hàng. Mọi đường dẫn dưới `https://danix.vn/api/open/v1`. Đặc tả máy đọc: [openapi.json](/openapi.json).

## Liệt kê đơn hàng {#listOrders}

`GET /orders`

Phân trang theo con trỏ, sắp theo lần sửa tăng dần. Đồng bộ tăng dần bằng `updatedSince` (lật trang trong một lượt bằng `nextCursor`); dòng danh sách không có `lines`, `payments` và `shippingAddress` — lấy chi tiết bằng `GET /orders/{id}`.

**Quyền cần có:** khoá có quyền `pos.orders.read`.

**Tham số query**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `limit` | integer | không | Số dòng mỗi trang, từ 1 đến 100. Mặc định 50. |
| `cursor` | string | không | Con trỏ lấy từ `nextCursor` của trang trước, để lật trang trong CÙNG một lượt đọc (giữ nguyên các tham số khác). Không tự dựng, không lưu để nối lượt đồng bộ sau. |
| `updatedSince` | string (date-time) | không | Chỉ lấy bản ghi có `updatedAt` từ thời điểm này (ISO 8601 UTC), tính cả mốc. Đây là cách đồng bộ tăng dần: có tham số này thì danh sách chỉ trả bản ghi đã qua khoảng trễ an toàn (trần thời gian tua lại kho của shop cộng 15 giây, mặc định 2 phút 15 giây). |
| `includeDeleted` | "true" \| "false" | không | `true` để lấy cả bản ghi đã xoá (có `deletedAt`). Mặc định `false`. |
| `status` | "new" \| "waiting_stock" \| "confirmed" \| "packing" \| "ready_to_ship" \| "shipped" \| "delivered" \| "paid" \| "returning" \| "partially_returned" \| "returned" \| "cancelled" \| "deleted" | không | Chỉ lấy đơn ở trạng thái này. |
| `customerId` | string (uuid) | không | Chỉ lấy đơn của khách này. |
| `code` | string | không | Tìm đúng mã đơn. |

**Ví dụ yêu cầu**

```bash
curl -X GET "https://danix.vn/api/open/v1/orders" \
  -H "Authorization: Bearer dnx_live_…"
```

**Phản hồi mẫu (200)**

```json
{
  "data": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000000101",
      "code": "DH1024",
      "status": "confirmed",
      "warehouseId": "018f3b8e-1c2d-7a4b-9c3d-000000000001",
      "warehouseName": "Kho chính",
      "customerId": "018f3b8e-1c2d-7a4b-9c3d-000000000201",
      "salesChannelId": "018f3b8e-1c2d-7a4b-9c3d-000000000301",
      "salesChannelName": "Facebook",
      "receivedAtShop": false,
      "billFullName": "Nguyễn Văn An",
      "billPhone": "0901234567",
      "billEmail": null,
      "totalPrice": "450000",
      "discount": "0",
      "shippingFee": "30000",
      "freeShipping": false,
      "surcharge": "0",
      "tax": "0",
      "totalAmount": "480000",
      "returnedAmount": "0",
      "exchangeReturn": false,
      "paidAmount": "0",
      "occurredAt": "2026-10-02T03:15:00.000Z",
      "note": null,
      "tags": [
        {
          "id": "018f3b8e-1c2d-7a4b-9c3d-000000000401",
          "name": "Khách quen"
        }
      ],
      "createdAt": "2026-10-02T03:15:00.000Z",
      "updatedAt": "2026-10-02T03:15:00.000Z",
      "deletedAt": null
    }
  ],
  "nextCursor": null
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `data` | array<object> | có | Các bản ghi của trang này. |
| `data[].id` | string (uuid) | có | Mã định danh của đơn. |
| `data[].code` | string | có | Mã đơn, duy nhất trong shop. |
| `data[].status` | "new" \| "waiting_stock" \| "confirmed" \| "packing" \| "ready_to_ship" \| "shipped" \| "delivered" \| "paid" \| "returning" \| "partially_returned" \| "returned" \| "cancelled" \| "deleted" | có | Trạng thái đơn. |
| `data[].warehouseId` | string (uuid) | có | Kho xuất hàng. |
| `data[].warehouseName` | string hoặc null | có | Tên kho xuất hàng. |
| `data[].customerId` | string (uuid) hoặc null | có | Khách hàng gắn với đơn. |
| `data[].salesChannelId` | string (uuid) hoặc null | có | Kênh bán. |
| `data[].salesChannelName` | string hoặc null | có | Tên kênh bán. |
| `data[].receivedAtShop` | boolean | có | Khách nhận hàng tại shop. |
| `data[].billFullName` | string hoặc null | có | Tên người mua ghi trên đơn. |
| `data[].billPhone` | string hoặc null | có | Số điện thoại người mua. |
| `data[].billEmail` | string hoặc null | có | Email người mua. |
| `data[].shippingAddress` | object hoặc null | không | Địa chỉ giao hàng. Chỉ có ở chi tiết đơn, không có trong danh sách. |
| `data[].shippingAddress.recipientName` | string hoặc null | có | Tên người nhận. |
| `data[].shippingAddress.recipientPhone` | string hoặc null | có | Số điện thoại người nhận. |
| `data[].shippingAddress.addressLine` | string hoặc null | có | Số nhà, đường. |
| `data[].shippingAddress.geoSystem` | "old" \| "new" hoặc null | có | Hệ địa giới của các mã bên dưới: `old` (tỉnh, huyện, xã) hoặc `new` (tỉnh, xã). `null` khi địa chỉ chỉ có chữ. |
| `data[].shippingAddress.provinceUnitId` | string (uuid) hoặc null | có | Mã (UUID) tỉnh hoặc thành phố; `null` khi không có. |
| `data[].shippingAddress.districtUnitId` | string (uuid) hoặc null | có | Mã (UUID) quận hoặc huyện; hệ `new` luôn `null`. |
| `data[].shippingAddress.wardUnitId` | string (uuid) hoặc null | có | Mã (UUID) phường hoặc xã; `null` khi không có. |
| `data[].shippingAddress.provinceName` | string hoặc null | có | Tỉnh hoặc thành phố. |
| `data[].shippingAddress.districtName` | string hoặc null | có | Quận hoặc huyện. |
| `data[].shippingAddress.wardName` | string hoặc null | có | Phường hoặc xã. |
| `data[].totalPrice` | string | có | Tiền hàng. Chuỗi thập phân, ví dụ "150000". |
| `data[].discount` | string | có | Giảm giá cả đơn. Chuỗi thập phân, ví dụ "150000". |
| `data[].shippingFee` | string | có | Phí vận chuyển khách trả. Chuỗi thập phân, ví dụ "150000". |
| `data[].freeShipping` | boolean | có | Shop chịu phí giao hàng. |
| `data[].surcharge` | string | có | Phụ thu. Chuỗi thập phân, ví dụ "150000". |
| `data[].tax` | string | có | Thuế. Chuỗi thập phân, ví dụ "150000". |
| `data[].totalAmount` | string | có | Tổng tiền đơn. Chuỗi thập phân, ví dụ "150000". |
| `data[].returnedAmount` | string | có | Tiền hàng khách đã hoàn. Chuỗi thập phân, ví dụ "150000". |
| `data[].paidAmount` | string | có | Đã thanh toán. Chuỗi thập phân, ví dụ "150000". |
| `data[].creditApplied` | string | không | Tiền cấn trừ vào đơn này từ phiếu đổi trả (khách trả hàng của một đơn khác): khách đã trả cho đơn này bằng khoản ấy, nên số còn phải thu đã trừ nó. Chỉ có ở chi tiết đơn, không có trong danh sách. Chuỗi thập phân, ví dụ "150000". |
| `data[].exchangeReturn` | boolean | không | Đơn trả để ĐỔI hàng: mọi lượt khách trả hàng của đơn đều là đổi (đổi size, đổi màu…), nên đơn không tính là hoàn — không vào tỉ lệ hoàn của số điện thoại, không gửi tin hoàn hay sự kiện hoàn sang Facebook. Chỉ có ở chi tiết đơn, không có trong danh sách. |
| `data[].occurredAt` | string (date-time) | có | Thời điểm phát sinh đơn. ISO 8601, múi giờ UTC. |
| `data[].note` | string hoặc null | có | Ghi chú nội bộ. |
| `data[].tags` | array<object> | có | Các thẻ của đơn. |
| `data[].tags[].id` | string (uuid) | có | Mã thẻ. |
| `data[].tags[].name` | string | có | Tên thẻ. |
| `data[].lines` | array<object> | không | Các dòng hàng. Chỉ có ở chi tiết đơn, không có trong danh sách. |
| `data[].lines[].id` | string (uuid) | có | Mã dòng hàng. |
| `data[].lines[].variantId` | string (uuid) | có | Mã mẫu mã. |
| `data[].lines[].productId` | string (uuid) hoặc null | có | Mã sản phẩm của mẫu mã. |
| `data[].lines[].variantCode` | string hoặc null | không | Mã mẫu mã. Vắng khi khoá thiếu `pos.products.read`. |
| `data[].lines[].variantName` | string hoặc null | không | Tên mẫu mã. Vắng khi khoá thiếu `pos.products.read`. |
| `data[].lines[].productName` | string hoặc null | không | Tên sản phẩm. Vắng khi khoá thiếu `pos.products.read`. |
| `data[].lines[].quantity` | string | có | Số lượng. Chuỗi thập phân, tối đa ba chữ số lẻ. |
| `data[].lines[].unitPrice` | string | có | Đơn giá. Chuỗi thập phân, ví dụ "150000". |
| `data[].lines[].discount` | string | có | Giảm giá của dòng. Chuỗi thập phân, ví dụ "150000". |
| `data[].lines[].lineTotal` | string | có | Thành tiền của dòng, đã trừ giảm giá. Chuỗi thập phân, ví dụ "150000". |
| `data[].lines[].returnedQuantity` | string | có | Số lượng khách đã hoàn. Chuỗi thập phân, tối đa ba chữ số lẻ. |
| `data[].payments` | array<object> | không | Các khoản thu. Chỉ có ở chi tiết đơn, không có trong danh sách. |
| `data[].payments[].methodId` | string (uuid) | có | Mã phương thức thanh toán. |
| `data[].payments[].methodName` | string | có | Tên phương thức thanh toán. |
| `data[].payments[].amount` | string | có | Số tiền thu theo phương thức này. Chuỗi thập phân, ví dụ "150000". |
| `data[].createdAt` | string (date-time) | có | Thời điểm tạo. ISO 8601, múi giờ UTC. |
| `data[].updatedAt` | string (date-time) | có | Thời điểm sửa gần nhất. ISO 8601, múi giờ UTC. |
| `data[].deletedAt` | string (date-time) hoặc null | có | Thời điểm xoá mềm; `null` nếu chưa xoá. ISO 8601 UTC hoặc `null`. |
| `nextCursor` | string hoặc null | có | Con trỏ của trang kế; `null` khi đã hết dữ liệu. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 400 | [`invalid-cursor`](/developers/errors#invalid-cursor) | Con trỏ phân trang không hợp lệ |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Tạo đơn hàng {#createOrder}

`POST /orders`

Tạo đơn mới ở trạng thái `new`. Gửi kèm header `Idempotency-Key` để thử lại an toàn: cùng khoá trong 24 giờ trả đúng đơn đã tạo, không tạo đơn thứ hai. `unitPrice` khác giá niêm yết của mẫu mã, hay giảm giá (của dòng hoặc cả đơn) khác 0, cần thêm quyền `pos.orders.price.override`; gửi `payments` cần thêm `pos.orders.payment.record` — thiếu thì `insufficient-permission`.

**Quyền cần có:** khoá có quyền `pos.orders.create` và quyền `pos.products.read`.

**Quyền thêm theo trường:** `pos.orders.payment.record`, `pos.orders.price.override` — chỉ cần khi thân dùng trường tương ứng (xem mô tả ở trên).

**Header**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | không | Khoá chống trùng do client đặt (khuyến nghị UUID). Gửi lại cùng khoá trong 24 giờ trả đúng kết quả lần đầu và header `Idempotent-Replayed: true`. |

**Thân yêu cầu (JSON)**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `warehouseId` | string (uuid) | có | Kho xuất hàng. |
| `customerId` | string (uuid) hoặc null | không | Khách hàng có sẵn. Bỏ trống nếu bán lẻ. |
| `salesChannelId` | string (uuid) hoặc null | không | Kênh bán. |
| `receivedAtShop` | boolean | không | Khách nhận hàng tại shop. |
| `billFullName` | string hoặc null | không | Tên người mua. |
| `billPhone` | string hoặc null | không | Số điện thoại người mua. |
| `billEmail` | string hoặc null | không | Email người mua. |
| `shippingAddress` | object hoặc null | không | Địa chỉ giao hàng. |
| `shippingAddress.recipientName` | string hoặc null | không | Tên người nhận. |
| `shippingAddress.recipientPhone` | string hoặc null | không | Số điện thoại người nhận. |
| `shippingAddress.addressLine` | string hoặc null | không | Số nhà, đường. |
| `shippingAddress.provinceName` | string hoặc null | không | Tỉnh hoặc thành phố. |
| `shippingAddress.districtName` | string hoặc null | không | Quận hoặc huyện. |
| `shippingAddress.wardName` | string hoặc null | không | Phường hoặc xã. |
| `shippingAddress.geoSystem` | "old" \| "new" hoặc null | không | Hệ địa giới của các mã bên dưới: `old` (tỉnh, huyện, xã) hoặc `new` (tỉnh, xã). Bỏ trống thì suy từ mã. |
| `shippingAddress.provinceUnitId` | string (uuid) hoặc null | không | Mã (UUID) tỉnh hoặc thành phố trong danh mục hành chính của DANIX. |
| `shippingAddress.districtUnitId` | string (uuid) hoặc null | không | Mã (UUID) quận hoặc huyện. Hệ `new` không có cấp này. |
| `shippingAddress.wardUnitId` | string (uuid) hoặc null | không | Mã (UUID) phường hoặc xã. |
| `lines` | array<object> | có | Các dòng hàng, 1 đến 200. |
| `lines[].variantId` | string (uuid) | có | Mã mẫu mã cần bán. |
| `lines[].quantity` | string | có | Số lượng, lớn hơn 0. Chuỗi thập phân, tối đa ba chữ số lẻ. |
| `lines[].unitPrice` | string | có | Đơn giá. Khác giá niêm yết của mẫu mã thì khoá cần thêm quyền `pos.orders.price.override`. Chuỗi thập phân, ví dụ "150000". |
| `lines[].discount` | string | không | Giảm giá của dòng. Mặc định "0". Khác 0 thì khoá cần thêm quyền `pos.orders.price.override`. |
| `discount` | string | không | Giảm giá cả đơn. Khác 0 thì khoá cần thêm quyền `pos.orders.price.override`. |
| `shippingFee` | string | không | Phí vận chuyển khách trả. |
| `freeShipping` | boolean | không | Shop chịu phí giao hàng. |
| `surcharge` | string | không | Phụ thu. |
| `tax` | string | không | Thuế. |
| `payments` | array<object> | không | Các khoản thu. Gửi khoản thu thì khoá cần thêm quyền `pos.orders.payment.record`. |
| `payments[].methodId` | string (uuid) | có | Mã phương thức thanh toán. |
| `payments[].amount` | string | có | Số tiền thu. Chuỗi thập phân, ví dụ "150000". |
| `occurredAt` | string (date-time) | không | Thời điểm phát sinh đơn. |
| `note` | string hoặc null | không | Ghi chú nội bộ. |
| `tagIds` | array<string (uuid)> | không | Các thẻ gắn vào đơn. |

**Ví dụ yêu cầu**

```bash
curl -X POST "https://danix.vn/api/open/v1/orders" \
  -H "Authorization: Bearer dnx_live_…" \
  -H "Idempotency-Key: cc8882df-37bd-41a7-9619-5f34df0a4f10" \
  -H "Content-Type: application/json" \
  -d '{
  "warehouseId": "018f3b8e-1c2d-7a4b-9c3d-000000000001",
  "billFullName": "Nguyễn Văn An",
  "billPhone": "0901234567",
  "lines": [
    {
      "variantId": "018f3b8e-1c2d-7a4b-9c3d-000000000601",
      "quantity": "2",
      "unitPrice": "225000"
    }
  ]
}'
```

**Phản hồi mẫu (201)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000000101",
  "code": "DH1024",
  "status": "confirmed",
  "warehouseId": "018f3b8e-1c2d-7a4b-9c3d-000000000001",
  "warehouseName": "Kho chính",
  "customerId": "018f3b8e-1c2d-7a4b-9c3d-000000000201",
  "salesChannelId": "018f3b8e-1c2d-7a4b-9c3d-000000000301",
  "salesChannelName": "Facebook",
  "receivedAtShop": false,
  "billFullName": "Nguyễn Văn An",
  "billPhone": "0901234567",
  "billEmail": null,
  "shippingAddress": {
    "recipientName": "Nguyễn Văn An",
    "recipientPhone": "0901234567",
    "addressLine": "12 Lê Lợi",
    "geoSystem": "old",
    "provinceUnitId": "018f3b8e-1c2d-7a4b-9c3d-000000001501",
    "districtUnitId": "018f3b8e-1c2d-7a4b-9c3d-000000001502",
    "wardUnitId": "018f3b8e-1c2d-7a4b-9c3d-000000001503",
    "provinceName": "Đà Nẵng",
    "districtName": "Hải Châu",
    "wardName": "Thạch Thang"
  },
  "totalPrice": "450000",
  "discount": "0",
  "shippingFee": "30000",
  "freeShipping": false,
  "surcharge": "0",
  "tax": "0",
  "totalAmount": "480000",
  "returnedAmount": "0",
  "exchangeReturn": false,
  "paidAmount": "0",
  "occurredAt": "2026-10-02T03:15:00.000Z",
  "note": null,
  "tags": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000000401",
      "name": "Khách quen"
    }
  ],
  "lines": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000000501",
      "variantId": "018f3b8e-1c2d-7a4b-9c3d-000000000601",
      "productId": "018f3b8e-1c2d-7a4b-9c3d-000000000701",
      "variantCode": "AO-THUN-M",
      "variantName": "Áo thun - M",
      "productName": "Áo thun cổ tròn",
      "quantity": "2",
      "unitPrice": "225000",
      "discount": "0",
      "lineTotal": "450000",
      "returnedQuantity": "0"
    }
  ],
  "payments": [],
  "createdAt": "2026-10-02T03:15:00.000Z",
  "updatedAt": "2026-10-02T03:15:00.000Z",
  "deletedAt": null
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh của đơn. |
| `code` | string | có | Mã đơn, duy nhất trong shop. |
| `status` | "new" \| "waiting_stock" \| "confirmed" \| "packing" \| "ready_to_ship" \| "shipped" \| "delivered" \| "paid" \| "returning" \| "partially_returned" \| "returned" \| "cancelled" \| "deleted" | có | Trạng thái đơn. |
| `warehouseId` | string (uuid) | có | Kho xuất hàng. |
| `warehouseName` | string hoặc null | có | Tên kho xuất hàng. |
| `customerId` | string (uuid) hoặc null | có | Khách hàng gắn với đơn. |
| `salesChannelId` | string (uuid) hoặc null | có | Kênh bán. |
| `salesChannelName` | string hoặc null | có | Tên kênh bán. |
| `receivedAtShop` | boolean | có | Khách nhận hàng tại shop. |
| `billFullName` | string hoặc null | có | Tên người mua ghi trên đơn. |
| `billPhone` | string hoặc null | có | Số điện thoại người mua. |
| `billEmail` | string hoặc null | có | Email người mua. |
| `shippingAddress` | object hoặc null | không | Địa chỉ giao hàng. Chỉ có ở chi tiết đơn, không có trong danh sách. |
| `shippingAddress.recipientName` | string hoặc null | có | Tên người nhận. |
| `shippingAddress.recipientPhone` | string hoặc null | có | Số điện thoại người nhận. |
| `shippingAddress.addressLine` | string hoặc null | có | Số nhà, đường. |
| `shippingAddress.geoSystem` | "old" \| "new" hoặc null | có | Hệ địa giới của các mã bên dưới: `old` (tỉnh, huyện, xã) hoặc `new` (tỉnh, xã). `null` khi địa chỉ chỉ có chữ. |
| `shippingAddress.provinceUnitId` | string (uuid) hoặc null | có | Mã (UUID) tỉnh hoặc thành phố; `null` khi không có. |
| `shippingAddress.districtUnitId` | string (uuid) hoặc null | có | Mã (UUID) quận hoặc huyện; hệ `new` luôn `null`. |
| `shippingAddress.wardUnitId` | string (uuid) hoặc null | có | Mã (UUID) phường hoặc xã; `null` khi không có. |
| `shippingAddress.provinceName` | string hoặc null | có | Tỉnh hoặc thành phố. |
| `shippingAddress.districtName` | string hoặc null | có | Quận hoặc huyện. |
| `shippingAddress.wardName` | string hoặc null | có | Phường hoặc xã. |
| `totalPrice` | string | có | Tiền hàng. Chuỗi thập phân, ví dụ "150000". |
| `discount` | string | có | Giảm giá cả đơn. Chuỗi thập phân, ví dụ "150000". |
| `shippingFee` | string | có | Phí vận chuyển khách trả. Chuỗi thập phân, ví dụ "150000". |
| `freeShipping` | boolean | có | Shop chịu phí giao hàng. |
| `surcharge` | string | có | Phụ thu. Chuỗi thập phân, ví dụ "150000". |
| `tax` | string | có | Thuế. Chuỗi thập phân, ví dụ "150000". |
| `totalAmount` | string | có | Tổng tiền đơn. Chuỗi thập phân, ví dụ "150000". |
| `returnedAmount` | string | có | Tiền hàng khách đã hoàn. Chuỗi thập phân, ví dụ "150000". |
| `paidAmount` | string | có | Đã thanh toán. Chuỗi thập phân, ví dụ "150000". |
| `creditApplied` | string | không | Tiền cấn trừ vào đơn này từ phiếu đổi trả (khách trả hàng của một đơn khác): khách đã trả cho đơn này bằng khoản ấy, nên số còn phải thu đã trừ nó. Chỉ có ở chi tiết đơn, không có trong danh sách. Chuỗi thập phân, ví dụ "150000". |
| `exchangeReturn` | boolean | không | Đơn trả để ĐỔI hàng: mọi lượt khách trả hàng của đơn đều là đổi (đổi size, đổi màu…), nên đơn không tính là hoàn — không vào tỉ lệ hoàn của số điện thoại, không gửi tin hoàn hay sự kiện hoàn sang Facebook. Chỉ có ở chi tiết đơn, không có trong danh sách. |
| `occurredAt` | string (date-time) | có | Thời điểm phát sinh đơn. ISO 8601, múi giờ UTC. |
| `note` | string hoặc null | có | Ghi chú nội bộ. |
| `tags` | array<object> | có | Các thẻ của đơn. |
| `tags[].id` | string (uuid) | có | Mã thẻ. |
| `tags[].name` | string | có | Tên thẻ. |
| `lines` | array<object> | không | Các dòng hàng. Chỉ có ở chi tiết đơn, không có trong danh sách. |
| `lines[].id` | string (uuid) | có | Mã dòng hàng. |
| `lines[].variantId` | string (uuid) | có | Mã mẫu mã. |
| `lines[].productId` | string (uuid) hoặc null | có | Mã sản phẩm của mẫu mã. |
| `lines[].variantCode` | string hoặc null | không | Mã mẫu mã. Vắng khi khoá thiếu `pos.products.read`. |
| `lines[].variantName` | string hoặc null | không | Tên mẫu mã. Vắng khi khoá thiếu `pos.products.read`. |
| `lines[].productName` | string hoặc null | không | Tên sản phẩm. Vắng khi khoá thiếu `pos.products.read`. |
| `lines[].quantity` | string | có | Số lượng. Chuỗi thập phân, tối đa ba chữ số lẻ. |
| `lines[].unitPrice` | string | có | Đơn giá. Chuỗi thập phân, ví dụ "150000". |
| `lines[].discount` | string | có | Giảm giá của dòng. Chuỗi thập phân, ví dụ "150000". |
| `lines[].lineTotal` | string | có | Thành tiền của dòng, đã trừ giảm giá. Chuỗi thập phân, ví dụ "150000". |
| `lines[].returnedQuantity` | string | có | Số lượng khách đã hoàn. Chuỗi thập phân, tối đa ba chữ số lẻ. |
| `payments` | array<object> | không | Các khoản thu. Chỉ có ở chi tiết đơn, không có trong danh sách. |
| `payments[].methodId` | string (uuid) | có | Mã phương thức thanh toán. |
| `payments[].methodName` | string | có | Tên phương thức thanh toán. |
| `payments[].amount` | string | có | Số tiền thu theo phương thức này. Chuỗi thập phân, ví dụ "150000". |
| `createdAt` | string (date-time) | có | Thời điểm tạo. ISO 8601, múi giờ UTC. |
| `updatedAt` | string (date-time) | có | Thời điểm sửa gần nhất. ISO 8601, múi giờ UTC. |
| `deletedAt` | string (date-time) hoặc null | có | Thời điểm xoá mềm; `null` nếu chưa xoá. ISO 8601 UTC hoặc `null`. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 413 | [`payload-too-large`](/developers/errors#payload-too-large) | Thân yêu cầu vượt quá dung lượng cho phép |
| 415 | [`unsupported-media-type`](/developers/errors#unsupported-media-type) | Thân yêu cầu phải là JSON (`Content-Type: application/json`) |
| 409 | [`conflict`](/developers/errors#conflict) | Thao tác xung đột với trạng thái hiện tại |
| 409 | [`idempotency-key-in-progress`](/developers/errors#idempotency-key-in-progress) | Yêu cầu với cùng Idempotency-Key đang được xử lý |
| 422 | [`idempotency-key-reused`](/developers/errors#idempotency-key-reused) | Idempotency-Key đã dùng cho một yêu cầu có nội dung khác |
| 422 | [`variant-removed`](/developers/errors#variant-removed) | Mẫu mã đã bị gỡ khỏi sản phẩm |
| 422 | [`customer-blocked`](/developers/errors#customer-blocked) | Khách hàng đang bị chặn |
| 422 | [`insufficient-stock`](/developers/errors#insufficient-stock) | Không đủ tồn kho cho thao tác này |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Chi tiết một đơn hàng {#getOrder}

`GET /orders/{id}`

Trả đơn kèm dòng hàng, khoản thu và địa chỉ giao hàng.

**Quyền cần có:** khoá có quyền `pos.orders.read`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Ví dụ yêu cầu**

```bash
curl -X GET "https://danix.vn/api/open/v1/orders/{id}" \
  -H "Authorization: Bearer dnx_live_…"
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (200)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000000101",
  "code": "DH1024",
  "status": "confirmed",
  "warehouseId": "018f3b8e-1c2d-7a4b-9c3d-000000000001",
  "warehouseName": "Kho chính",
  "customerId": "018f3b8e-1c2d-7a4b-9c3d-000000000201",
  "salesChannelId": "018f3b8e-1c2d-7a4b-9c3d-000000000301",
  "salesChannelName": "Facebook",
  "receivedAtShop": false,
  "billFullName": "Nguyễn Văn An",
  "billPhone": "0901234567",
  "billEmail": null,
  "shippingAddress": {
    "recipientName": "Nguyễn Văn An",
    "recipientPhone": "0901234567",
    "addressLine": "12 Lê Lợi",
    "geoSystem": "old",
    "provinceUnitId": "018f3b8e-1c2d-7a4b-9c3d-000000001501",
    "districtUnitId": "018f3b8e-1c2d-7a4b-9c3d-000000001502",
    "wardUnitId": "018f3b8e-1c2d-7a4b-9c3d-000000001503",
    "provinceName": "Đà Nẵng",
    "districtName": "Hải Châu",
    "wardName": "Thạch Thang"
  },
  "totalPrice": "450000",
  "discount": "0",
  "shippingFee": "30000",
  "freeShipping": false,
  "surcharge": "0",
  "tax": "0",
  "totalAmount": "480000",
  "returnedAmount": "0",
  "exchangeReturn": false,
  "paidAmount": "0",
  "occurredAt": "2026-10-02T03:15:00.000Z",
  "note": null,
  "tags": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000000401",
      "name": "Khách quen"
    }
  ],
  "lines": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000000501",
      "variantId": "018f3b8e-1c2d-7a4b-9c3d-000000000601",
      "productId": "018f3b8e-1c2d-7a4b-9c3d-000000000701",
      "variantCode": "AO-THUN-M",
      "variantName": "Áo thun - M",
      "productName": "Áo thun cổ tròn",
      "quantity": "2",
      "unitPrice": "225000",
      "discount": "0",
      "lineTotal": "450000",
      "returnedQuantity": "0"
    }
  ],
  "payments": [],
  "createdAt": "2026-10-02T03:15:00.000Z",
  "updatedAt": "2026-10-02T03:15:00.000Z",
  "deletedAt": null
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh của đơn. |
| `code` | string | có | Mã đơn, duy nhất trong shop. |
| `status` | "new" \| "waiting_stock" \| "confirmed" \| "packing" \| "ready_to_ship" \| "shipped" \| "delivered" \| "paid" \| "returning" \| "partially_returned" \| "returned" \| "cancelled" \| "deleted" | có | Trạng thái đơn. |
| `warehouseId` | string (uuid) | có | Kho xuất hàng. |
| `warehouseName` | string hoặc null | có | Tên kho xuất hàng. |
| `customerId` | string (uuid) hoặc null | có | Khách hàng gắn với đơn. |
| `salesChannelId` | string (uuid) hoặc null | có | Kênh bán. |
| `salesChannelName` | string hoặc null | có | Tên kênh bán. |
| `receivedAtShop` | boolean | có | Khách nhận hàng tại shop. |
| `billFullName` | string hoặc null | có | Tên người mua ghi trên đơn. |
| `billPhone` | string hoặc null | có | Số điện thoại người mua. |
| `billEmail` | string hoặc null | có | Email người mua. |
| `shippingAddress` | object hoặc null | không | Địa chỉ giao hàng. Chỉ có ở chi tiết đơn, không có trong danh sách. |
| `shippingAddress.recipientName` | string hoặc null | có | Tên người nhận. |
| `shippingAddress.recipientPhone` | string hoặc null | có | Số điện thoại người nhận. |
| `shippingAddress.addressLine` | string hoặc null | có | Số nhà, đường. |
| `shippingAddress.geoSystem` | "old" \| "new" hoặc null | có | Hệ địa giới của các mã bên dưới: `old` (tỉnh, huyện, xã) hoặc `new` (tỉnh, xã). `null` khi địa chỉ chỉ có chữ. |
| `shippingAddress.provinceUnitId` | string (uuid) hoặc null | có | Mã (UUID) tỉnh hoặc thành phố; `null` khi không có. |
| `shippingAddress.districtUnitId` | string (uuid) hoặc null | có | Mã (UUID) quận hoặc huyện; hệ `new` luôn `null`. |
| `shippingAddress.wardUnitId` | string (uuid) hoặc null | có | Mã (UUID) phường hoặc xã; `null` khi không có. |
| `shippingAddress.provinceName` | string hoặc null | có | Tỉnh hoặc thành phố. |
| `shippingAddress.districtName` | string hoặc null | có | Quận hoặc huyện. |
| `shippingAddress.wardName` | string hoặc null | có | Phường hoặc xã. |
| `totalPrice` | string | có | Tiền hàng. Chuỗi thập phân, ví dụ "150000". |
| `discount` | string | có | Giảm giá cả đơn. Chuỗi thập phân, ví dụ "150000". |
| `shippingFee` | string | có | Phí vận chuyển khách trả. Chuỗi thập phân, ví dụ "150000". |
| `freeShipping` | boolean | có | Shop chịu phí giao hàng. |
| `surcharge` | string | có | Phụ thu. Chuỗi thập phân, ví dụ "150000". |
| `tax` | string | có | Thuế. Chuỗi thập phân, ví dụ "150000". |
| `totalAmount` | string | có | Tổng tiền đơn. Chuỗi thập phân, ví dụ "150000". |
| `returnedAmount` | string | có | Tiền hàng khách đã hoàn. Chuỗi thập phân, ví dụ "150000". |
| `paidAmount` | string | có | Đã thanh toán. Chuỗi thập phân, ví dụ "150000". |
| `creditApplied` | string | không | Tiền cấn trừ vào đơn này từ phiếu đổi trả (khách trả hàng của một đơn khác): khách đã trả cho đơn này bằng khoản ấy, nên số còn phải thu đã trừ nó. Chỉ có ở chi tiết đơn, không có trong danh sách. Chuỗi thập phân, ví dụ "150000". |
| `exchangeReturn` | boolean | không | Đơn trả để ĐỔI hàng: mọi lượt khách trả hàng của đơn đều là đổi (đổi size, đổi màu…), nên đơn không tính là hoàn — không vào tỉ lệ hoàn của số điện thoại, không gửi tin hoàn hay sự kiện hoàn sang Facebook. Chỉ có ở chi tiết đơn, không có trong danh sách. |
| `occurredAt` | string (date-time) | có | Thời điểm phát sinh đơn. ISO 8601, múi giờ UTC. |
| `note` | string hoặc null | có | Ghi chú nội bộ. |
| `tags` | array<object> | có | Các thẻ của đơn. |
| `tags[].id` | string (uuid) | có | Mã thẻ. |
| `tags[].name` | string | có | Tên thẻ. |
| `lines` | array<object> | không | Các dòng hàng. Chỉ có ở chi tiết đơn, không có trong danh sách. |
| `lines[].id` | string (uuid) | có | Mã dòng hàng. |
| `lines[].variantId` | string (uuid) | có | Mã mẫu mã. |
| `lines[].productId` | string (uuid) hoặc null | có | Mã sản phẩm của mẫu mã. |
| `lines[].variantCode` | string hoặc null | không | Mã mẫu mã. Vắng khi khoá thiếu `pos.products.read`. |
| `lines[].variantName` | string hoặc null | không | Tên mẫu mã. Vắng khi khoá thiếu `pos.products.read`. |
| `lines[].productName` | string hoặc null | không | Tên sản phẩm. Vắng khi khoá thiếu `pos.products.read`. |
| `lines[].quantity` | string | có | Số lượng. Chuỗi thập phân, tối đa ba chữ số lẻ. |
| `lines[].unitPrice` | string | có | Đơn giá. Chuỗi thập phân, ví dụ "150000". |
| `lines[].discount` | string | có | Giảm giá của dòng. Chuỗi thập phân, ví dụ "150000". |
| `lines[].lineTotal` | string | có | Thành tiền của dòng, đã trừ giảm giá. Chuỗi thập phân, ví dụ "150000". |
| `lines[].returnedQuantity` | string | có | Số lượng khách đã hoàn. Chuỗi thập phân, tối đa ba chữ số lẻ. |
| `payments` | array<object> | không | Các khoản thu. Chỉ có ở chi tiết đơn, không có trong danh sách. |
| `payments[].methodId` | string (uuid) | có | Mã phương thức thanh toán. |
| `payments[].methodName` | string | có | Tên phương thức thanh toán. |
| `payments[].amount` | string | có | Số tiền thu theo phương thức này. Chuỗi thập phân, ví dụ "150000". |
| `createdAt` | string (date-time) | có | Thời điểm tạo. ISO 8601, múi giờ UTC. |
| `updatedAt` | string (date-time) | có | Thời điểm sửa gần nhất. ISO 8601, múi giờ UTC. |
| `deletedAt` | string (date-time) hoặc null | có | Thời điểm xoá mềm; `null` nếu chưa xoá. ISO 8601 UTC hoặc `null`. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Sửa đơn hàng {#updateOrder}

`PATCH /orders/{id}`

Chỉ gửi trường cần đổi. Dòng hàng và kho không sửa được qua đường này. Đổi `discount` cần thêm quyền `pos.orders.price.override`.

**Quyền cần có:** khoá có quyền `pos.orders.update` và quyền `pos.products.read`.

**Quyền thêm theo trường:** `pos.orders.price.override` — chỉ cần khi thân dùng trường tương ứng (xem mô tả ở trên).

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Thân yêu cầu (JSON)**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `customerId` | string (uuid) hoặc null | không | Đổi khách hàng gắn với đơn. |
| `salesChannelId` | string (uuid) hoặc null | không | Đổi kênh bán. |
| `billFullName` | string hoặc null | không | Tên người mua. |
| `billPhone` | string hoặc null | không | Số điện thoại người mua. |
| `billEmail` | string hoặc null | không | Email người mua. |
| `shippingAddress` | object hoặc null | không | Địa chỉ giao hàng. |
| `shippingAddress.recipientName` | string hoặc null | không | Tên người nhận. |
| `shippingAddress.recipientPhone` | string hoặc null | không | Số điện thoại người nhận. |
| `shippingAddress.addressLine` | string hoặc null | không | Số nhà, đường. |
| `shippingAddress.provinceName` | string hoặc null | không | Tỉnh hoặc thành phố. |
| `shippingAddress.districtName` | string hoặc null | không | Quận hoặc huyện. |
| `shippingAddress.wardName` | string hoặc null | không | Phường hoặc xã. |
| `shippingAddress.geoSystem` | "old" \| "new" hoặc null | không | Hệ địa giới của các mã bên dưới: `old` (tỉnh, huyện, xã) hoặc `new` (tỉnh, xã). Bỏ trống thì suy từ mã. |
| `shippingAddress.provinceUnitId` | string (uuid) hoặc null | không | Mã (UUID) tỉnh hoặc thành phố trong danh mục hành chính của DANIX. |
| `shippingAddress.districtUnitId` | string (uuid) hoặc null | không | Mã (UUID) quận hoặc huyện. Hệ `new` không có cấp này. |
| `shippingAddress.wardUnitId` | string (uuid) hoặc null | không | Mã (UUID) phường hoặc xã. |
| `discount` | string | không | Giảm giá cả đơn. Đổi giảm giá thì khoá cần thêm quyền `pos.orders.price.override`. |
| `shippingFee` | string | không | Phí vận chuyển khách trả. |
| `surcharge` | string | không | Phụ thu. |
| `tax` | string | không | Thuế. |
| `note` | string hoặc null | không | Ghi chú nội bộ. |
| `tagIds` | array<string (uuid)> | không | Thay toàn bộ thẻ của đơn. |

**Ví dụ yêu cầu**

```bash
curl -X PATCH "https://danix.vn/api/open/v1/orders/{id}" \
  -H "Authorization: Bearer dnx_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "note": "Khách dặn gọi trước khi giao"
}'
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (200)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000000101",
  "code": "DH1024",
  "status": "confirmed",
  "warehouseId": "018f3b8e-1c2d-7a4b-9c3d-000000000001",
  "warehouseName": "Kho chính",
  "customerId": "018f3b8e-1c2d-7a4b-9c3d-000000000201",
  "salesChannelId": "018f3b8e-1c2d-7a4b-9c3d-000000000301",
  "salesChannelName": "Facebook",
  "receivedAtShop": false,
  "billFullName": "Nguyễn Văn An",
  "billPhone": "0901234567",
  "billEmail": null,
  "shippingAddress": {
    "recipientName": "Nguyễn Văn An",
    "recipientPhone": "0901234567",
    "addressLine": "12 Lê Lợi",
    "geoSystem": "old",
    "provinceUnitId": "018f3b8e-1c2d-7a4b-9c3d-000000001501",
    "districtUnitId": "018f3b8e-1c2d-7a4b-9c3d-000000001502",
    "wardUnitId": "018f3b8e-1c2d-7a4b-9c3d-000000001503",
    "provinceName": "Đà Nẵng",
    "districtName": "Hải Châu",
    "wardName": "Thạch Thang"
  },
  "totalPrice": "450000",
  "discount": "0",
  "shippingFee": "30000",
  "freeShipping": false,
  "surcharge": "0",
  "tax": "0",
  "totalAmount": "480000",
  "returnedAmount": "0",
  "exchangeReturn": false,
  "paidAmount": "0",
  "occurredAt": "2026-10-02T03:15:00.000Z",
  "note": null,
  "tags": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000000401",
      "name": "Khách quen"
    }
  ],
  "lines": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000000501",
      "variantId": "018f3b8e-1c2d-7a4b-9c3d-000000000601",
      "productId": "018f3b8e-1c2d-7a4b-9c3d-000000000701",
      "variantCode": "AO-THUN-M",
      "variantName": "Áo thun - M",
      "productName": "Áo thun cổ tròn",
      "quantity": "2",
      "unitPrice": "225000",
      "discount": "0",
      "lineTotal": "450000",
      "returnedQuantity": "0"
    }
  ],
  "payments": [],
  "createdAt": "2026-10-02T03:15:00.000Z",
  "updatedAt": "2026-10-02T03:15:00.000Z",
  "deletedAt": null
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh của đơn. |
| `code` | string | có | Mã đơn, duy nhất trong shop. |
| `status` | "new" \| "waiting_stock" \| "confirmed" \| "packing" \| "ready_to_ship" \| "shipped" \| "delivered" \| "paid" \| "returning" \| "partially_returned" \| "returned" \| "cancelled" \| "deleted" | có | Trạng thái đơn. |
| `warehouseId` | string (uuid) | có | Kho xuất hàng. |
| `warehouseName` | string hoặc null | có | Tên kho xuất hàng. |
| `customerId` | string (uuid) hoặc null | có | Khách hàng gắn với đơn. |
| `salesChannelId` | string (uuid) hoặc null | có | Kênh bán. |
| `salesChannelName` | string hoặc null | có | Tên kênh bán. |
| `receivedAtShop` | boolean | có | Khách nhận hàng tại shop. |
| `billFullName` | string hoặc null | có | Tên người mua ghi trên đơn. |
| `billPhone` | string hoặc null | có | Số điện thoại người mua. |
| `billEmail` | string hoặc null | có | Email người mua. |
| `shippingAddress` | object hoặc null | không | Địa chỉ giao hàng. Chỉ có ở chi tiết đơn, không có trong danh sách. |
| `shippingAddress.recipientName` | string hoặc null | có | Tên người nhận. |
| `shippingAddress.recipientPhone` | string hoặc null | có | Số điện thoại người nhận. |
| `shippingAddress.addressLine` | string hoặc null | có | Số nhà, đường. |
| `shippingAddress.geoSystem` | "old" \| "new" hoặc null | có | Hệ địa giới của các mã bên dưới: `old` (tỉnh, huyện, xã) hoặc `new` (tỉnh, xã). `null` khi địa chỉ chỉ có chữ. |
| `shippingAddress.provinceUnitId` | string (uuid) hoặc null | có | Mã (UUID) tỉnh hoặc thành phố; `null` khi không có. |
| `shippingAddress.districtUnitId` | string (uuid) hoặc null | có | Mã (UUID) quận hoặc huyện; hệ `new` luôn `null`. |
| `shippingAddress.wardUnitId` | string (uuid) hoặc null | có | Mã (UUID) phường hoặc xã; `null` khi không có. |
| `shippingAddress.provinceName` | string hoặc null | có | Tỉnh hoặc thành phố. |
| `shippingAddress.districtName` | string hoặc null | có | Quận hoặc huyện. |
| `shippingAddress.wardName` | string hoặc null | có | Phường hoặc xã. |
| `totalPrice` | string | có | Tiền hàng. Chuỗi thập phân, ví dụ "150000". |
| `discount` | string | có | Giảm giá cả đơn. Chuỗi thập phân, ví dụ "150000". |
| `shippingFee` | string | có | Phí vận chuyển khách trả. Chuỗi thập phân, ví dụ "150000". |
| `freeShipping` | boolean | có | Shop chịu phí giao hàng. |
| `surcharge` | string | có | Phụ thu. Chuỗi thập phân, ví dụ "150000". |
| `tax` | string | có | Thuế. Chuỗi thập phân, ví dụ "150000". |
| `totalAmount` | string | có | Tổng tiền đơn. Chuỗi thập phân, ví dụ "150000". |
| `returnedAmount` | string | có | Tiền hàng khách đã hoàn. Chuỗi thập phân, ví dụ "150000". |
| `paidAmount` | string | có | Đã thanh toán. Chuỗi thập phân, ví dụ "150000". |
| `creditApplied` | string | không | Tiền cấn trừ vào đơn này từ phiếu đổi trả (khách trả hàng của một đơn khác): khách đã trả cho đơn này bằng khoản ấy, nên số còn phải thu đã trừ nó. Chỉ có ở chi tiết đơn, không có trong danh sách. Chuỗi thập phân, ví dụ "150000". |
| `exchangeReturn` | boolean | không | Đơn trả để ĐỔI hàng: mọi lượt khách trả hàng của đơn đều là đổi (đổi size, đổi màu…), nên đơn không tính là hoàn — không vào tỉ lệ hoàn của số điện thoại, không gửi tin hoàn hay sự kiện hoàn sang Facebook. Chỉ có ở chi tiết đơn, không có trong danh sách. |
| `occurredAt` | string (date-time) | có | Thời điểm phát sinh đơn. ISO 8601, múi giờ UTC. |
| `note` | string hoặc null | có | Ghi chú nội bộ. |
| `tags` | array<object> | có | Các thẻ của đơn. |
| `tags[].id` | string (uuid) | có | Mã thẻ. |
| `tags[].name` | string | có | Tên thẻ. |
| `lines` | array<object> | không | Các dòng hàng. Chỉ có ở chi tiết đơn, không có trong danh sách. |
| `lines[].id` | string (uuid) | có | Mã dòng hàng. |
| `lines[].variantId` | string (uuid) | có | Mã mẫu mã. |
| `lines[].productId` | string (uuid) hoặc null | có | Mã sản phẩm của mẫu mã. |
| `lines[].variantCode` | string hoặc null | không | Mã mẫu mã. Vắng khi khoá thiếu `pos.products.read`. |
| `lines[].variantName` | string hoặc null | không | Tên mẫu mã. Vắng khi khoá thiếu `pos.products.read`. |
| `lines[].productName` | string hoặc null | không | Tên sản phẩm. Vắng khi khoá thiếu `pos.products.read`. |
| `lines[].quantity` | string | có | Số lượng. Chuỗi thập phân, tối đa ba chữ số lẻ. |
| `lines[].unitPrice` | string | có | Đơn giá. Chuỗi thập phân, ví dụ "150000". |
| `lines[].discount` | string | có | Giảm giá của dòng. Chuỗi thập phân, ví dụ "150000". |
| `lines[].lineTotal` | string | có | Thành tiền của dòng, đã trừ giảm giá. Chuỗi thập phân, ví dụ "150000". |
| `lines[].returnedQuantity` | string | có | Số lượng khách đã hoàn. Chuỗi thập phân, tối đa ba chữ số lẻ. |
| `payments` | array<object> | không | Các khoản thu. Chỉ có ở chi tiết đơn, không có trong danh sách. |
| `payments[].methodId` | string (uuid) | có | Mã phương thức thanh toán. |
| `payments[].methodName` | string | có | Tên phương thức thanh toán. |
| `payments[].amount` | string | có | Số tiền thu theo phương thức này. Chuỗi thập phân, ví dụ "150000". |
| `createdAt` | string (date-time) | có | Thời điểm tạo. ISO 8601, múi giờ UTC. |
| `updatedAt` | string (date-time) | có | Thời điểm sửa gần nhất. ISO 8601, múi giờ UTC. |
| `deletedAt` | string (date-time) hoặc null | có | Thời điểm xoá mềm; `null` nếu chưa xoá. ISO 8601 UTC hoặc `null`. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 413 | [`payload-too-large`](/developers/errors#payload-too-large) | Thân yêu cầu vượt quá dung lượng cho phép |
| 415 | [`unsupported-media-type`](/developers/errors#unsupported-media-type) | Thân yêu cầu phải là JSON (`Content-Type: application/json`) |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |
| 409 | [`conflict`](/developers/errors#conflict) | Thao tác xung đột với trạng thái hiện tại |
| 422 | [`customer-blocked`](/developers/errors#customer-blocked) | Khách hàng đang bị chặn |
| 409 | [`order-has-live-shipment`](/developers/errors#order-has-live-shipment) | Đơn đang có vận đơn chưa kết thúc nên không thao tác được |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Đổi trạng thái đơn {#changeOrderStatus}

`POST /orders/{id}/status`

Chuyển đơn sang trạng thái mới theo luồng của shop. Chuyển trạng thái có thể xuất hoặc nhập kho; từ chối bằng `order-status-not-allowed` hoặc `insufficient-stock`. Huỷ một đơn đã gửi hàng (`shipped`, `delivered`, `paid`, `returning`, `partially_returned`, `returned` sang `cancelled`; hàng khách đang giữ được nhập lại kho) cần thêm quyền `pos.orders.delete` — thiếu thì `insufficient-permission`. Đơn đã rời `new` thì không đưa về `new` được nữa (`order-status-not-allowed`).

**Quyền cần có:** khoá có quyền `pos.orders.status` và quyền `pos.products.read`.

**Quyền thêm theo trường:** `pos.orders.delete` — chỉ cần khi thân dùng trường tương ứng (xem mô tả ở trên).

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Thân yêu cầu (JSON)**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `status` | "new" \| "waiting_stock" \| "confirmed" \| "packing" \| "ready_to_ship" \| "shipped" \| "delivered" \| "paid" \| "returning" \| "returned" \| "cancelled" | có | Trạng thái mới của đơn. `new` không bao giờ là một đích đi được: đơn đang ở `new` → `conflict`; đơn đã rời `new` → `order-status-not-allowed`. |

**Ví dụ yêu cầu**

```bash
curl -X POST "https://danix.vn/api/open/v1/orders/{id}/status" \
  -H "Authorization: Bearer dnx_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "status": "confirmed"
}'
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (200)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000000101",
  "code": "DH1024",
  "status": "confirmed",
  "warehouseId": "018f3b8e-1c2d-7a4b-9c3d-000000000001",
  "warehouseName": "Kho chính",
  "customerId": "018f3b8e-1c2d-7a4b-9c3d-000000000201",
  "salesChannelId": "018f3b8e-1c2d-7a4b-9c3d-000000000301",
  "salesChannelName": "Facebook",
  "receivedAtShop": false,
  "billFullName": "Nguyễn Văn An",
  "billPhone": "0901234567",
  "billEmail": null,
  "shippingAddress": {
    "recipientName": "Nguyễn Văn An",
    "recipientPhone": "0901234567",
    "addressLine": "12 Lê Lợi",
    "geoSystem": "old",
    "provinceUnitId": "018f3b8e-1c2d-7a4b-9c3d-000000001501",
    "districtUnitId": "018f3b8e-1c2d-7a4b-9c3d-000000001502",
    "wardUnitId": "018f3b8e-1c2d-7a4b-9c3d-000000001503",
    "provinceName": "Đà Nẵng",
    "districtName": "Hải Châu",
    "wardName": "Thạch Thang"
  },
  "totalPrice": "450000",
  "discount": "0",
  "shippingFee": "30000",
  "freeShipping": false,
  "surcharge": "0",
  "tax": "0",
  "totalAmount": "480000",
  "returnedAmount": "0",
  "exchangeReturn": false,
  "paidAmount": "0",
  "occurredAt": "2026-10-02T03:15:00.000Z",
  "note": null,
  "tags": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000000401",
      "name": "Khách quen"
    }
  ],
  "lines": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000000501",
      "variantId": "018f3b8e-1c2d-7a4b-9c3d-000000000601",
      "productId": "018f3b8e-1c2d-7a4b-9c3d-000000000701",
      "variantCode": "AO-THUN-M",
      "variantName": "Áo thun - M",
      "productName": "Áo thun cổ tròn",
      "quantity": "2",
      "unitPrice": "225000",
      "discount": "0",
      "lineTotal": "450000",
      "returnedQuantity": "0"
    }
  ],
  "payments": [],
  "createdAt": "2026-10-02T03:15:00.000Z",
  "updatedAt": "2026-10-02T03:15:00.000Z",
  "deletedAt": null
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh của đơn. |
| `code` | string | có | Mã đơn, duy nhất trong shop. |
| `status` | "new" \| "waiting_stock" \| "confirmed" \| "packing" \| "ready_to_ship" \| "shipped" \| "delivered" \| "paid" \| "returning" \| "partially_returned" \| "returned" \| "cancelled" \| "deleted" | có | Trạng thái đơn. |
| `warehouseId` | string (uuid) | có | Kho xuất hàng. |
| `warehouseName` | string hoặc null | có | Tên kho xuất hàng. |
| `customerId` | string (uuid) hoặc null | có | Khách hàng gắn với đơn. |
| `salesChannelId` | string (uuid) hoặc null | có | Kênh bán. |
| `salesChannelName` | string hoặc null | có | Tên kênh bán. |
| `receivedAtShop` | boolean | có | Khách nhận hàng tại shop. |
| `billFullName` | string hoặc null | có | Tên người mua ghi trên đơn. |
| `billPhone` | string hoặc null | có | Số điện thoại người mua. |
| `billEmail` | string hoặc null | có | Email người mua. |
| `shippingAddress` | object hoặc null | không | Địa chỉ giao hàng. Chỉ có ở chi tiết đơn, không có trong danh sách. |
| `shippingAddress.recipientName` | string hoặc null | có | Tên người nhận. |
| `shippingAddress.recipientPhone` | string hoặc null | có | Số điện thoại người nhận. |
| `shippingAddress.addressLine` | string hoặc null | có | Số nhà, đường. |
| `shippingAddress.geoSystem` | "old" \| "new" hoặc null | có | Hệ địa giới của các mã bên dưới: `old` (tỉnh, huyện, xã) hoặc `new` (tỉnh, xã). `null` khi địa chỉ chỉ có chữ. |
| `shippingAddress.provinceUnitId` | string (uuid) hoặc null | có | Mã (UUID) tỉnh hoặc thành phố; `null` khi không có. |
| `shippingAddress.districtUnitId` | string (uuid) hoặc null | có | Mã (UUID) quận hoặc huyện; hệ `new` luôn `null`. |
| `shippingAddress.wardUnitId` | string (uuid) hoặc null | có | Mã (UUID) phường hoặc xã; `null` khi không có. |
| `shippingAddress.provinceName` | string hoặc null | có | Tỉnh hoặc thành phố. |
| `shippingAddress.districtName` | string hoặc null | có | Quận hoặc huyện. |
| `shippingAddress.wardName` | string hoặc null | có | Phường hoặc xã. |
| `totalPrice` | string | có | Tiền hàng. Chuỗi thập phân, ví dụ "150000". |
| `discount` | string | có | Giảm giá cả đơn. Chuỗi thập phân, ví dụ "150000". |
| `shippingFee` | string | có | Phí vận chuyển khách trả. Chuỗi thập phân, ví dụ "150000". |
| `freeShipping` | boolean | có | Shop chịu phí giao hàng. |
| `surcharge` | string | có | Phụ thu. Chuỗi thập phân, ví dụ "150000". |
| `tax` | string | có | Thuế. Chuỗi thập phân, ví dụ "150000". |
| `totalAmount` | string | có | Tổng tiền đơn. Chuỗi thập phân, ví dụ "150000". |
| `returnedAmount` | string | có | Tiền hàng khách đã hoàn. Chuỗi thập phân, ví dụ "150000". |
| `paidAmount` | string | có | Đã thanh toán. Chuỗi thập phân, ví dụ "150000". |
| `creditApplied` | string | không | Tiền cấn trừ vào đơn này từ phiếu đổi trả (khách trả hàng của một đơn khác): khách đã trả cho đơn này bằng khoản ấy, nên số còn phải thu đã trừ nó. Chỉ có ở chi tiết đơn, không có trong danh sách. Chuỗi thập phân, ví dụ "150000". |
| `exchangeReturn` | boolean | không | Đơn trả để ĐỔI hàng: mọi lượt khách trả hàng của đơn đều là đổi (đổi size, đổi màu…), nên đơn không tính là hoàn — không vào tỉ lệ hoàn của số điện thoại, không gửi tin hoàn hay sự kiện hoàn sang Facebook. Chỉ có ở chi tiết đơn, không có trong danh sách. |
| `occurredAt` | string (date-time) | có | Thời điểm phát sinh đơn. ISO 8601, múi giờ UTC. |
| `note` | string hoặc null | có | Ghi chú nội bộ. |
| `tags` | array<object> | có | Các thẻ của đơn. |
| `tags[].id` | string (uuid) | có | Mã thẻ. |
| `tags[].name` | string | có | Tên thẻ. |
| `lines` | array<object> | không | Các dòng hàng. Chỉ có ở chi tiết đơn, không có trong danh sách. |
| `lines[].id` | string (uuid) | có | Mã dòng hàng. |
| `lines[].variantId` | string (uuid) | có | Mã mẫu mã. |
| `lines[].productId` | string (uuid) hoặc null | có | Mã sản phẩm của mẫu mã. |
| `lines[].variantCode` | string hoặc null | không | Mã mẫu mã. Vắng khi khoá thiếu `pos.products.read`. |
| `lines[].variantName` | string hoặc null | không | Tên mẫu mã. Vắng khi khoá thiếu `pos.products.read`. |
| `lines[].productName` | string hoặc null | không | Tên sản phẩm. Vắng khi khoá thiếu `pos.products.read`. |
| `lines[].quantity` | string | có | Số lượng. Chuỗi thập phân, tối đa ba chữ số lẻ. |
| `lines[].unitPrice` | string | có | Đơn giá. Chuỗi thập phân, ví dụ "150000". |
| `lines[].discount` | string | có | Giảm giá của dòng. Chuỗi thập phân, ví dụ "150000". |
| `lines[].lineTotal` | string | có | Thành tiền của dòng, đã trừ giảm giá. Chuỗi thập phân, ví dụ "150000". |
| `lines[].returnedQuantity` | string | có | Số lượng khách đã hoàn. Chuỗi thập phân, tối đa ba chữ số lẻ. |
| `payments` | array<object> | không | Các khoản thu. Chỉ có ở chi tiết đơn, không có trong danh sách. |
| `payments[].methodId` | string (uuid) | có | Mã phương thức thanh toán. |
| `payments[].methodName` | string | có | Tên phương thức thanh toán. |
| `payments[].amount` | string | có | Số tiền thu theo phương thức này. Chuỗi thập phân, ví dụ "150000". |
| `createdAt` | string (date-time) | có | Thời điểm tạo. ISO 8601, múi giờ UTC. |
| `updatedAt` | string (date-time) | có | Thời điểm sửa gần nhất. ISO 8601, múi giờ UTC. |
| `deletedAt` | string (date-time) hoặc null | có | Thời điểm xoá mềm; `null` nếu chưa xoá. ISO 8601 UTC hoặc `null`. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 413 | [`payload-too-large`](/developers/errors#payload-too-large) | Thân yêu cầu vượt quá dung lượng cho phép |
| 415 | [`unsupported-media-type`](/developers/errors#unsupported-media-type) | Thân yêu cầu phải là JSON (`Content-Type: application/json`) |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |
| 409 | [`conflict`](/developers/errors#conflict) | Thao tác xung đột với trạng thái hiện tại |
| 422 | [`order-status-not-allowed`](/developers/errors#order-status-not-allowed) | Không chuyển được đơn sang trạng thái này từ trạng thái hiện tại |
| 422 | [`insufficient-stock`](/developers/errors#insufficient-stock) | Không đủ tồn kho cho thao tác này |
| 422 | [`variant-removed`](/developers/errors#variant-removed) | Mẫu mã đã bị gỡ khỏi sản phẩm |
| 409 | [`order-has-live-shipment`](/developers/errors#order-has-live-shipment) | Đơn đang có vận đơn chưa kết thúc nên không thao tác được |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Ghi chú của đơn {#listOrderNotes}

`GET /orders/{id}/notes`

Danh sách ghi chú, mới nhất trước.

**Quyền cần có:** khoá có quyền `pos.orders.read`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Ví dụ yêu cầu**

```bash
curl -X GET "https://danix.vn/api/open/v1/orders/{id}/notes" \
  -H "Authorization: Bearer dnx_live_…"
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (200)**

```json
{
  "data": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000000777",
      "message": "Đã gọi khách xác nhận",
      "createdByName": "Đồng bộ kế toán",
      "createdByIsIntegration": true,
      "createdAt": "2026-10-02T03:15:00.000Z",
      "updatedAt": "2026-10-02T03:15:00.000Z"
    }
  ]
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `data` | array<object> | có | Các ghi chú, mới nhất trước. |
| `data[].id` | string (uuid) | có | Mã ghi chú. |
| `data[].message` | string | có | Nội dung ghi chú. |
| `data[].createdByName` | string hoặc null | có | Tên người hay ứng dụng đã ghi. |
| `data[].createdByIsIntegration` | boolean | có | `true` khi ghi chú do một ứng dụng kết nối tạo. |
| `data[].createdAt` | string (date-time) | có | Thời điểm tạo. ISO 8601, múi giờ UTC. |
| `data[].updatedAt` | string (date-time) | có | Thời điểm sửa gần nhất. ISO 8601, múi giờ UTC. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Thêm ghi chú vào đơn {#createOrderNote}

`POST /orders/{id}/notes`

Ghi chú đứng tên ứng dụng kết nối.

**Quyền cần có:** khoá có quyền `pos.orders.update`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Thân yêu cầu (JSON)**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `message` | string | có | Nội dung ghi chú. |

**Ví dụ yêu cầu**

```bash
curl -X POST "https://danix.vn/api/open/v1/orders/{id}/notes" \
  -H "Authorization: Bearer dnx_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "message": "Đã gọi khách xác nhận"
}'
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (201)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000000777",
  "message": "Đã gọi khách xác nhận",
  "createdByName": "Đồng bộ kế toán",
  "createdByIsIntegration": true,
  "createdAt": "2026-10-02T03:15:00.000Z",
  "updatedAt": "2026-10-02T03:15:00.000Z"
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã ghi chú. |
| `message` | string | có | Nội dung ghi chú. |
| `createdByName` | string hoặc null | có | Tên người hay ứng dụng đã ghi. |
| `createdByIsIntegration` | boolean | có | `true` khi ghi chú do một ứng dụng kết nối tạo. |
| `createdAt` | string (date-time) | có | Thời điểm tạo. ISO 8601, múi giờ UTC. |
| `updatedAt` | string (date-time) | có | Thời điểm sửa gần nhất. ISO 8601, múi giờ UTC. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 413 | [`payload-too-large`](/developers/errors#payload-too-large) | Thân yêu cầu vượt quá dung lượng cho phép |
| 415 | [`unsupported-media-type`](/developers/errors#unsupported-media-type) | Thân yêu cầu phải là JSON (`Content-Type: application/json`) |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

---

Nguồn: https://danix.vn/developers/reference/products.md

# Tham chiếu: Sản phẩm

Sản phẩm, mẫu mã và danh mục. Mọi đường dẫn dưới `https://danix.vn/api/open/v1`. Đặc tả máy đọc: [openapi.json](/openapi.json).

## Liệt kê sản phẩm {#listProducts}

`GET /products`

Phân trang theo con trỏ, sắp theo lần sửa tăng dần. Dòng danh sách gọn; lấy mẫu mã bằng `GET /products/{id}`.

**Quyền cần có:** khoá có quyền `pos.products.read`.

**Tham số query**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `limit` | integer | không | Số dòng mỗi trang, từ 1 đến 100. Mặc định 50. |
| `cursor` | string | không | Con trỏ lấy từ `nextCursor` của trang trước, để lật trang trong CÙNG một lượt đọc (giữ nguyên các tham số khác). Không tự dựng, không lưu để nối lượt đồng bộ sau. |
| `updatedSince` | string (date-time) | không | Chỉ lấy bản ghi có `updatedAt` từ thời điểm này (ISO 8601 UTC), tính cả mốc. Đây là cách đồng bộ tăng dần: có tham số này thì danh sách chỉ trả bản ghi đã qua khoảng trễ an toàn (trần thời gian tua lại kho của shop cộng 15 giây, mặc định 2 phút 15 giây). |
| `includeDeleted` | "true" \| "false" | không | `true` để lấy cả bản ghi đã xoá (có `deletedAt`). Mặc định `false`. |
| `search` | string | không | Tìm theo tên hoặc mã sản phẩm. |
| `categoryId` | string (uuid) | không | Chỉ lấy sản phẩm trong danh mục này. |

**Ví dụ yêu cầu**

```bash
curl -X GET "https://danix.vn/api/open/v1/products" \
  -H "Authorization: Bearer dnx_live_…"
```

**Phản hồi mẫu (200)**

```json
{
  "data": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000000701",
      "code": "SP0001",
      "name": "Áo thun cổ tròn",
      "type": "simple",
      "isActive": true,
      "categoryNames": [
        "Áo"
      ],
      "variantCount": 1,
      "imageUrl": null,
      "priceFrom": "225000",
      "priceTo": "225000",
      "updatedAt": "2026-10-02T03:15:00.000Z",
      "deletedAt": null
    }
  ],
  "nextCursor": null
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `data` | array<object> | có | Các bản ghi của trang này. |
| `data[].id` | string (uuid) | có | Mã sản phẩm. |
| `data[].code` | string | có | Mã sản phẩm, duy nhất trong shop. |
| `data[].name` | string | có | Tên sản phẩm. |
| `data[].type` | "simple" \| "service" | có | Loại: `simple` (hàng hoá) hoặc `service` (dịch vụ). |
| `data[].isActive` | boolean | có | Sản phẩm còn bán. |
| `data[].categoryNames` | array<string> | có | Tên các danh mục chứa sản phẩm. |
| `data[].variantCount` | integer | có | Số mẫu mã. |
| `data[].imageUrl` | string hoặc null | có | Ảnh đại diện. |
| `data[].priceFrom` | string hoặc null | có | Giá bán thấp nhất trong các mẫu mã. Chuỗi thập phân hoặc `null`. |
| `data[].priceTo` | string hoặc null | có | Giá bán cao nhất trong các mẫu mã. Chuỗi thập phân hoặc `null`. |
| `data[].updatedAt` | string (date-time) | có | Lần sửa gần nhất. Danh sách sắp theo `(updatedAt, id)`; dùng làm mốc `updatedSince`. ISO 8601, múi giờ UTC. |
| `data[].deletedAt` | string (date-time) hoặc null | có | Thời điểm xoá mềm; `null` nếu chưa xoá. Chỉ khác `null` khi gọi với `includeDeleted=true`. ISO 8601 UTC hoặc `null`. |
| `nextCursor` | string hoặc null | có | Con trỏ của trang kế; `null` khi đã hết dữ liệu. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 400 | [`invalid-cursor`](/developers/errors#invalid-cursor) | Con trỏ phân trang không hợp lệ |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Tạo sản phẩm {#createProduct}

`POST /products`

Tạo sản phẩm kèm ít nhất một mẫu mã. Sản phẩm nhiều mẫu mã: mỗi mẫu mang `attributes` theo tên (`{ "Size": "M" }`), mọi mẫu cùng một bộ thuộc tính; thuộc tính hay giá trị chưa có thì được tạo (cần thêm quyền `pos.products.update`).

**Quyền cần có:** khoá có quyền `pos.products.create` và quyền `pos.products.read`.

**Quyền thêm theo trường:** `pos.products.update` — chỉ cần khi thân dùng trường tương ứng (xem mô tả ở trên).

**Thân yêu cầu (JSON)**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `code` | string | không | Mã sản phẩm. Bỏ trống thì hệ thống tự cấp. |
| `name` | string | có | Tên sản phẩm. |
| `description` | string hoặc null | không | Mô tả. |
| `categoryIds` | array<string (uuid)> | không | Các danh mục. |
| `type` | "simple" \| "service" | không | Loại sản phẩm. Mặc định `simple`. |
| `variants` | array<object> | có | Các mẫu mã, ít nhất một. Nhiều mẫu mã thì mỗi mẫu mang `attributes` để phân biệt; thứ tự thuộc tính của sản phẩm theo thứ tự khoá ở mẫu mã đầu tiên. |
| `variants[].id` | string (uuid) | không | Mã mẫu mã cần sửa; bỏ trống để thêm mẫu mã mới. |
| `variants[].code` | string | không | Mã mẫu mã (SKU). Bỏ trống thì hệ thống tự cấp. |
| `variants[].barcode` | string hoặc null | không | Mã vạch. |
| `variants[].price` | string | không | Giá bán. |
| `variants[].weightGrams` | string hoặc null | không | Cân nặng, gram. |
| `variants[].isActive` | boolean | không | Mẫu mã còn bán. |
| `variants[].attributes` | object | không | Thuộc tính theo TÊN, ví dụ `{ "Size": "M", "Màu": "Đỏ" }` (tên và giá trị không phân biệt hoa thường; chưa có thì được tạo, cần thêm quyền `pos.products.update`). Mọi mẫu mã của một sản phẩm mang đúng cùng một bộ thuộc tính, không hai mẫu trùng tổ hợp. Gửi kèm `id` là đổi tổ hợp của mẫu mã ấy. |

**Ví dụ yêu cầu**

```bash
curl -X POST "https://danix.vn/api/open/v1/products" \
  -H "Authorization: Bearer dnx_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Áo thun cổ tròn",
  "variants": [
    {
      "code": "AO-THUN-M",
      "price": "225000",
      "attributes": {
        "Size": "M"
      }
    }
  ]
}'
```

**Phản hồi mẫu (201)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000000701",
  "code": "SP0001",
  "name": "Áo thun cổ tròn",
  "description": null,
  "type": "simple",
  "isActive": true,
  "categoryIds": [
    "018f3b8e-1c2d-7a4b-9c3d-000000000801"
  ],
  "variants": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000000601",
      "code": "AO-THUN-M",
      "barcode": null,
      "price": "225000",
      "weightGrams": "200",
      "lengthCm": null,
      "widthCm": null,
      "heightCm": null,
      "isActive": true,
      "attributes": {
        "Size": "M"
      }
    }
  ],
  "images": [],
  "createdAt": "2026-10-02T03:15:00.000Z",
  "deletedAt": null
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã sản phẩm. |
| `code` | string | có | Mã sản phẩm, duy nhất trong shop. |
| `name` | string | có | Tên sản phẩm. |
| `description` | string hoặc null | có | Mô tả. |
| `type` | "simple" \| "service" | có | Loại: `simple` (hàng hoá) hoặc `service` (dịch vụ). |
| `isActive` | boolean | có | Sản phẩm còn bán. |
| `categoryIds` | array<string (uuid)> | có | Các danh mục chứa sản phẩm. |
| `variants` | array<object> | có | Các mẫu mã. |
| `variants[].id` | string (uuid) | có | Mã mẫu mã. |
| `variants[].code` | string | có | Mã mẫu mã (SKU), duy nhất trong shop. |
| `variants[].barcode` | string hoặc null | có | Mã vạch. |
| `variants[].price` | string | có | Giá bán. Chuỗi thập phân, ví dụ "150000". |
| `variants[].weightGrams` | string hoặc null | có | Cân nặng, gram. Chuỗi thập phân. |
| `variants[].lengthCm` | string hoặc null | có | Chiều dài, cm. |
| `variants[].widthCm` | string hoặc null | có | Chiều rộng, cm. |
| `variants[].heightCm` | string hoặc null | có | Chiều cao, cm. |
| `variants[].isActive` | boolean | có | Mẫu mã còn bán. |
| `variants[].attributes` | object | có | Thuộc tính của mẫu mã theo TÊN, ví dụ `{ "Size": "M", "Màu": "Đỏ" }`, theo thứ tự thuộc tính của sản phẩm. Rỗng với sản phẩm không có thuộc tính. |
| `images` | array<object> | có | Ảnh và video. |
| `images[].id` | string (uuid) | có | Mã tệp. |
| `images[].url` | string | có | Đường dẫn tuyệt đối tới tệp. |
| `images[].kind` | "image" \| "video" | có | Loại tệp. |
| `images[].variantIds` | array<string (uuid)> | có | Các mẫu mã dùng tệp này; rỗng là tệp chung. |
| `createdAt` | string (date-time) | có | Thời điểm tạo. ISO 8601, múi giờ UTC. |
| `deletedAt` | string (date-time) hoặc null | có | Thời điểm xoá mềm; `null` nếu chưa xoá. ISO 8601 UTC hoặc `null`. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 413 | [`payload-too-large`](/developers/errors#payload-too-large) | Thân yêu cầu vượt quá dung lượng cho phép |
| 415 | [`unsupported-media-type`](/developers/errors#unsupported-media-type) | Thân yêu cầu phải là JSON (`Content-Type: application/json`) |
| 409 | [`duplicate-code`](/developers/errors#duplicate-code) | Mã đã tồn tại trong shop |
| 409 | [`conflict`](/developers/errors#conflict) | Thao tác xung đột với trạng thái hiện tại |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Chi tiết một sản phẩm {#getProduct}

`GET /products/{id}`

Trả sản phẩm kèm mẫu mã, ảnh và danh mục.

**Quyền cần có:** khoá có quyền `pos.products.read`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Ví dụ yêu cầu**

```bash
curl -X GET "https://danix.vn/api/open/v1/products/{id}" \
  -H "Authorization: Bearer dnx_live_…"
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (200)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000000701",
  "code": "SP0001",
  "name": "Áo thun cổ tròn",
  "description": null,
  "type": "simple",
  "isActive": true,
  "categoryIds": [
    "018f3b8e-1c2d-7a4b-9c3d-000000000801"
  ],
  "variants": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000000601",
      "code": "AO-THUN-M",
      "barcode": null,
      "price": "225000",
      "weightGrams": "200",
      "lengthCm": null,
      "widthCm": null,
      "heightCm": null,
      "isActive": true,
      "attributes": {
        "Size": "M"
      }
    }
  ],
  "images": [],
  "createdAt": "2026-10-02T03:15:00.000Z",
  "deletedAt": null
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã sản phẩm. |
| `code` | string | có | Mã sản phẩm, duy nhất trong shop. |
| `name` | string | có | Tên sản phẩm. |
| `description` | string hoặc null | có | Mô tả. |
| `type` | "simple" \| "service" | có | Loại: `simple` (hàng hoá) hoặc `service` (dịch vụ). |
| `isActive` | boolean | có | Sản phẩm còn bán. |
| `categoryIds` | array<string (uuid)> | có | Các danh mục chứa sản phẩm. |
| `variants` | array<object> | có | Các mẫu mã. |
| `variants[].id` | string (uuid) | có | Mã mẫu mã. |
| `variants[].code` | string | có | Mã mẫu mã (SKU), duy nhất trong shop. |
| `variants[].barcode` | string hoặc null | có | Mã vạch. |
| `variants[].price` | string | có | Giá bán. Chuỗi thập phân, ví dụ "150000". |
| `variants[].weightGrams` | string hoặc null | có | Cân nặng, gram. Chuỗi thập phân. |
| `variants[].lengthCm` | string hoặc null | có | Chiều dài, cm. |
| `variants[].widthCm` | string hoặc null | có | Chiều rộng, cm. |
| `variants[].heightCm` | string hoặc null | có | Chiều cao, cm. |
| `variants[].isActive` | boolean | có | Mẫu mã còn bán. |
| `variants[].attributes` | object | có | Thuộc tính của mẫu mã theo TÊN, ví dụ `{ "Size": "M", "Màu": "Đỏ" }`, theo thứ tự thuộc tính của sản phẩm. Rỗng với sản phẩm không có thuộc tính. |
| `images` | array<object> | có | Ảnh và video. |
| `images[].id` | string (uuid) | có | Mã tệp. |
| `images[].url` | string | có | Đường dẫn tuyệt đối tới tệp. |
| `images[].kind` | "image" \| "video" | có | Loại tệp. |
| `images[].variantIds` | array<string (uuid)> | có | Các mẫu mã dùng tệp này; rỗng là tệp chung. |
| `createdAt` | string (date-time) | có | Thời điểm tạo. ISO 8601, múi giờ UTC. |
| `deletedAt` | string (date-time) hoặc null | có | Thời điểm xoá mềm; `null` nếu chưa xoá. ISO 8601 UTC hoặc `null`. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Sửa sản phẩm {#updateProduct}

`PATCH /products/{id}`

Chỉ gửi trường cần đổi. `variants` vá TỪNG PHẦN: mẫu mã có `id` chỉ đổi các trường gửi lên, mẫu mã không có `id` được thêm vào cuối (kèm `attributes` theo tên), mẫu mã đã có mà không nhắc tới giữ nguyên. Gỡ mẫu mã chưa mở ở phiên bản này.

**Quyền cần có:** khoá có quyền `pos.products.update` và quyền `pos.products.read`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Thân yêu cầu (JSON)**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `name` | string | không | Tên sản phẩm. |
| `description` | string hoặc null | không | Mô tả. |
| `categoryIds` | array<string (uuid)> | không | Thay toàn bộ danh mục. |
| `variants` | array<object> | không | Vá TỪNG PHẦN: mẫu mã có `id` được sửa (chỉ các trường gửi lên), mẫu mã không có `id` được thêm vào cuối; mẫu mã đã có mà không nhắc tới thì GIỮ NGUYÊN. Gỡ mẫu mã chưa mở ở phiên bản này. |
| `variants[].id` | string (uuid) | không | Mã mẫu mã cần sửa; bỏ trống để thêm mẫu mã mới. |
| `variants[].code` | string | không | Mã mẫu mã (SKU). Bỏ trống thì hệ thống tự cấp. |
| `variants[].barcode` | string hoặc null | không | Mã vạch. |
| `variants[].price` | string | không | Giá bán. |
| `variants[].weightGrams` | string hoặc null | không | Cân nặng, gram. |
| `variants[].isActive` | boolean | không | Mẫu mã còn bán. |
| `variants[].attributes` | object | không | Thuộc tính theo TÊN, ví dụ `{ "Size": "M", "Màu": "Đỏ" }` (tên và giá trị không phân biệt hoa thường; chưa có thì được tạo, cần thêm quyền `pos.products.update`). Mọi mẫu mã của một sản phẩm mang đúng cùng một bộ thuộc tính, không hai mẫu trùng tổ hợp. Gửi kèm `id` là đổi tổ hợp của mẫu mã ấy. |

**Ví dụ yêu cầu**

```bash
curl -X PATCH "https://danix.vn/api/open/v1/products/{id}" \
  -H "Authorization: Bearer dnx_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Áo thun cổ tròn cotton",
  "variants": [
    {
      "code": "AO-THUN-L",
      "price": "235000",
      "attributes": {
        "Size": "L"
      }
    }
  ]
}'
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (200)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000000701",
  "code": "SP0001",
  "name": "Áo thun cổ tròn",
  "description": null,
  "type": "simple",
  "isActive": true,
  "categoryIds": [
    "018f3b8e-1c2d-7a4b-9c3d-000000000801"
  ],
  "variants": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000000601",
      "code": "AO-THUN-M",
      "barcode": null,
      "price": "225000",
      "weightGrams": "200",
      "lengthCm": null,
      "widthCm": null,
      "heightCm": null,
      "isActive": true,
      "attributes": {
        "Size": "M"
      }
    }
  ],
  "images": [],
  "createdAt": "2026-10-02T03:15:00.000Z",
  "deletedAt": null
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã sản phẩm. |
| `code` | string | có | Mã sản phẩm, duy nhất trong shop. |
| `name` | string | có | Tên sản phẩm. |
| `description` | string hoặc null | có | Mô tả. |
| `type` | "simple" \| "service" | có | Loại: `simple` (hàng hoá) hoặc `service` (dịch vụ). |
| `isActive` | boolean | có | Sản phẩm còn bán. |
| `categoryIds` | array<string (uuid)> | có | Các danh mục chứa sản phẩm. |
| `variants` | array<object> | có | Các mẫu mã. |
| `variants[].id` | string (uuid) | có | Mã mẫu mã. |
| `variants[].code` | string | có | Mã mẫu mã (SKU), duy nhất trong shop. |
| `variants[].barcode` | string hoặc null | có | Mã vạch. |
| `variants[].price` | string | có | Giá bán. Chuỗi thập phân, ví dụ "150000". |
| `variants[].weightGrams` | string hoặc null | có | Cân nặng, gram. Chuỗi thập phân. |
| `variants[].lengthCm` | string hoặc null | có | Chiều dài, cm. |
| `variants[].widthCm` | string hoặc null | có | Chiều rộng, cm. |
| `variants[].heightCm` | string hoặc null | có | Chiều cao, cm. |
| `variants[].isActive` | boolean | có | Mẫu mã còn bán. |
| `variants[].attributes` | object | có | Thuộc tính của mẫu mã theo TÊN, ví dụ `{ "Size": "M", "Màu": "Đỏ" }`, theo thứ tự thuộc tính của sản phẩm. Rỗng với sản phẩm không có thuộc tính. |
| `images` | array<object> | có | Ảnh và video. |
| `images[].id` | string (uuid) | có | Mã tệp. |
| `images[].url` | string | có | Đường dẫn tuyệt đối tới tệp. |
| `images[].kind` | "image" \| "video" | có | Loại tệp. |
| `images[].variantIds` | array<string (uuid)> | có | Các mẫu mã dùng tệp này; rỗng là tệp chung. |
| `createdAt` | string (date-time) | có | Thời điểm tạo. ISO 8601, múi giờ UTC. |
| `deletedAt` | string (date-time) hoặc null | có | Thời điểm xoá mềm; `null` nếu chưa xoá. ISO 8601 UTC hoặc `null`. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 413 | [`payload-too-large`](/developers/errors#payload-too-large) | Thân yêu cầu vượt quá dung lượng cho phép |
| 415 | [`unsupported-media-type`](/developers/errors#unsupported-media-type) | Thân yêu cầu phải là JSON (`Content-Type: application/json`) |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |
| 409 | [`duplicate-code`](/developers/errors#duplicate-code) | Mã đã tồn tại trong shop |
| 409 | [`conflict`](/developers/errors#conflict) | Thao tác xung đột với trạng thái hiện tại |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Danh mục sản phẩm {#listCategories}

`GET /categories`

Toàn bộ danh mục của shop, dạng cây phẳng với `parentId`.

**Quyền cần có:** khoá có quyền `pos.products.read`.

**Ví dụ yêu cầu**

```bash
curl -X GET "https://danix.vn/api/open/v1/categories" \
  -H "Authorization: Bearer dnx_live_…"
```

**Phản hồi mẫu (200)**

```json
{
  "data": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000000801",
      "parentId": null,
      "name": "Áo",
      "slug": "ao"
    }
  ]
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `data` | array<object> | có | Các danh mục. |
| `data[].id` | string (uuid) | có | Mã danh mục. |
| `data[].parentId` | string (uuid) hoặc null | có | Danh mục cha. |
| `data[].name` | string | có | Tên danh mục. |
| `data[].slug` | string | có | Tên ngắn trong đường dẫn. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

---

Nguồn: https://danix.vn/developers/reference/inventory.md

# Tham chiếu: Kho

Kho, tồn kho và phiếu nhập, xuất, kiểm kho. Mọi đường dẫn dưới `https://danix.vn/api/open/v1`. Đặc tả máy đọc: [openapi.json](/openapi.json).

## Danh sách kho {#listWarehouses}

`GET /warehouses`

Các kho hàng của shop.

**Quyền cần có:** khoá có quyền `pos.inventory.read`.

**Ví dụ yêu cầu**

```bash
curl -X GET "https://danix.vn/api/open/v1/warehouses" \
  -H "Authorization: Bearer dnx_live_…"
```

**Phản hồi mẫu (200)**

```json
{
  "data": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000000001",
      "code": "KHO1",
      "name": "Kho chính",
      "phone": null,
      "addressLine": null,
      "provinceName": null,
      "districtName": null,
      "wardName": null,
      "isDefault": true
    }
  ]
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `data` | array<object> | có | Các kho. |
| `data[].id` | string (uuid) | có | Mã kho. |
| `data[].code` | string | có | Mã kho. |
| `data[].name` | string | có | Tên kho. |
| `data[].phone` | string hoặc null | có | Số điện thoại kho. |
| `data[].addressLine` | string hoặc null | có | Địa chỉ kho. |
| `data[].provinceName` | string hoặc null | có | Tỉnh hoặc thành phố. |
| `data[].districtName` | string hoặc null | có | Quận hoặc huyện. |
| `data[].wardName` | string hoặc null | có | Phường hoặc xã. |
| `data[].isDefault` | boolean | có | Kho mặc định của shop. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Tồn kho theo mẫu mã và kho {#listStock}

`GET /inventory/stock`

Mỗi dòng là tồn của một mẫu mã tại một kho. Không có giá vốn. Đồng bộ tăng dần bằng `updatedSince` (lật trang trong một lượt bằng `nextCursor`): có `updatedSince` thì chỉ trả dòng tồn đổi từ mốc ấy và đã qua khoảng trễ an toàn (bằng trần thời gian tua lại kho của shop cộng 15 giây, mặc định 2 phút 15 giây). Mốc của dòng tồn chỉ đổi khi số tồn hoặc `inTransit` đổi; giá, mã, mã vạch, tên và ảnh đi kèm lấy từ đồng bộ sản phẩm và danh sách kho.

**Quyền cần có:** khoá có quyền `pos.inventory.read` và quyền `pos.products.read`.

**Tham số query**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `limit` | integer | không | Số dòng mỗi trang, từ 1 đến 100. Mặc định 50. |
| `cursor` | string | không | Con trỏ lấy từ `nextCursor` của trang trước, để lật trang trong CÙNG một lượt đọc (giữ nguyên các tham số khác). Không tự dựng, không lưu để nối lượt đồng bộ sau. |
| `updatedSince` | string (date-time) | không | Chỉ lấy bản ghi có `updatedAt` từ thời điểm này (ISO 8601 UTC), tính cả mốc. Đây là cách đồng bộ tăng dần: có tham số này thì danh sách chỉ trả bản ghi đã qua khoảng trễ an toàn (trần thời gian tua lại kho của shop cộng 15 giây, mặc định 2 phút 15 giây). |
| `warehouseId` | string (uuid) | không | Chỉ lấy tồn của kho này. |
| `search` | string | không | Tìm theo tên hoặc mã mẫu mã. |

**Ví dụ yêu cầu**

```bash
curl -X GET "https://danix.vn/api/open/v1/inventory/stock" \
  -H "Authorization: Bearer dnx_live_…"
```

**Phản hồi mẫu (200)**

```json
{
  "data": [
    {
      "variantId": "018f3b8e-1c2d-7a4b-9c3d-000000000601",
      "productId": "018f3b8e-1c2d-7a4b-9c3d-000000000701",
      "productName": "Áo thun cổ tròn",
      "variantName": "Áo thun - M",
      "code": "AO-THUN-M",
      "barcode": null,
      "warehouseId": "018f3b8e-1c2d-7a4b-9c3d-000000000001",
      "warehouseName": "Kho chính",
      "quantity": "18",
      "inTransit": "0",
      "price": "225000"
    }
  ],
  "nextCursor": null
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `data` | array<object> | có | Các bản ghi của trang này. |
| `data[].variantId` | string (uuid) | có | Mã mẫu mã. |
| `data[].productId` | string (uuid) | có | Mã sản phẩm. |
| `data[].productName` | string | có | Tên sản phẩm. |
| `data[].variantName` | string | có | Tên mẫu mã. |
| `data[].code` | string | có | Mã mẫu mã (SKU). |
| `data[].barcode` | string hoặc null | có | Mã vạch. |
| `data[].warehouseId` | string (uuid) | có | Mã kho. |
| `data[].warehouseName` | string | có | Tên kho. |
| `data[].quantity` | string | có | Tồn thực trong kho. Chuỗi thập phân, tối đa ba chữ số lẻ. |
| `data[].inTransit` | string | có | Số lượng đang trên đường về kho. Chuỗi thập phân, tối đa ba chữ số lẻ. |
| `data[].price` | string | có | Giá bán. Chuỗi thập phân, ví dụ "150000". |
| `nextCursor` | string hoặc null | có | Con trỏ của trang kế; `null` khi đã hết dữ liệu. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 400 | [`invalid-cursor`](/developers/errors#invalid-cursor) | Con trỏ phân trang không hợp lệ |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Tạo phiếu kho nháp {#createInventoryDocument}

`POST /inventory/documents`

Tạo phiếu nhập, xuất hoặc kiểm kho ở trạng thái `draft`. Tồn kho chỉ đổi khi ghi sổ bằng `POST /inventory/documents/{id}/post`.

**Quyền cần có:** khoá có ít nhất một trong các quyền `pos.inventory.receipt`, `pos.inventory.adjust` và đủ các quyền `pos.inventory.read`, `pos.products.read`.

**Thân yêu cầu (JSON)**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `kind` | "receipt" \| "issue" \| "stocktake" | có | Loại phiếu: nhập, xuất hoặc kiểm kho. |
| `warehouseId` | string (uuid) | có | Kho của phiếu. |
| `supplierId` | string (uuid) hoặc null | không | Nhà cung cấp (phiếu nhập). |
| `documentDate` | string | không | Ngày của phiếu, dạng YYYY-MM-DD. |
| `note` | string hoặc null | không | Ghi chú. |
| `lines` | array<object> | không | Các dòng của phiếu, tối đa 500. |
| `lines[].variantId` | string (uuid) | có | Mã mẫu mã. |
| `lines[].quantity` | string | không | Số lượng nhập hoặc xuất. |
| `lines[].countedQuantity` | string | không | Số đếm thực tế (phiếu kiểm kho). |
| `lines[].unitCost` | string | không | Giá nhập (phiếu nhập). |
| `lines[].note` | string hoặc null | không | Ghi chú dòng. |

**Ví dụ yêu cầu**

```bash
curl -X POST "https://danix.vn/api/open/v1/inventory/documents" \
  -H "Authorization: Bearer dnx_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "kind": "receipt",
  "warehouseId": "018f3b8e-1c2d-7a4b-9c3d-000000000001",
  "lines": [
    {
      "variantId": "018f3b8e-1c2d-7a4b-9c3d-000000000601",
      "quantity": "10",
      "unitCost": "120000"
    }
  ]
}'
```

**Phản hồi mẫu (201)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000001201",
  "kind": "receipt",
  "code": "PN0001",
  "status": "draft",
  "warehouseId": "018f3b8e-1c2d-7a4b-9c3d-000000000001",
  "supplierId": null,
  "occurredAt": "2026-10-02T03:15:00.000Z",
  "postedAt": null,
  "costingMethod": null,
  "note": null,
  "lines": []
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã phiếu. |
| `kind` | string | có | Loại phiếu: `receipt`, `issue`, `stocktake`… |
| `code` | string | có | Mã phiếu. |
| `status` | "draft" \| "ordered" \| "posted" \| "in_transit" \| "completed" \| "cancelled" | có | Trạng thái phiếu: `draft` là nháp, `posted` đã ghi sổ. |
| `warehouseId` | string (uuid) | có | Kho của phiếu. |
| `supplierId` | string (uuid) hoặc null | có | Nhà cung cấp. |
| `occurredAt` | string (date-time) | có | Ngày của phiếu. ISO 8601, múi giờ UTC. |
| `postedAt` | string (date-time) hoặc null | có | Thời điểm ghi sổ. ISO 8601 UTC hoặc `null`. |
| `costingMethod` | "average" \| "fifo" hoặc null | có | Phương pháp tính giá vốn khi ghi sổ. |
| `note` | string hoặc null | có | Ghi chú phiếu. |
| `lines` | array<object> | có | Các dòng của phiếu. |
| `lines[].id` | string (uuid) | có | Mã dòng phiếu. |
| `lines[].variantId` | string (uuid) | có | Mã mẫu mã. |
| `lines[].variantCode` | string | có | Mã mẫu mã. |
| `lines[].quantity` | string hoặc null | có | Số lượng nhập hoặc xuất. Chuỗi thập phân. |
| `lines[].countedQuantity` | string hoặc null | có | Số đếm thực tế (phiếu kiểm kho). |
| `lines[].unitCost` | string hoặc null | không | Giá nhập. Chỉ có khi khoá được xem giá vốn. |
| `lines[].note` | string hoặc null | có | Ghi chú dòng. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 413 | [`payload-too-large`](/developers/errors#payload-too-large) | Thân yêu cầu vượt quá dung lượng cho phép |
| 415 | [`unsupported-media-type`](/developers/errors#unsupported-media-type) | Thân yêu cầu phải là JSON (`Content-Type: application/json`) |
| 422 | [`variant-removed`](/developers/errors#variant-removed) | Mẫu mã đã bị gỡ khỏi sản phẩm |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Ghi sổ phiếu kho {#postInventoryDocument}

`POST /inventory/documents/{id}/post`

Ghi sổ phiếu nháp: tồn kho đổi ngay. Ghi sổ lần hai trả `document-already-posted`. Quyền theo LOẠI phiếu: phiếu kiểm kho cần `pos.inventory.adjust`, phiếu nhập hay xuất cần `pos.inventory.receipt` — có quyền còn lại thôi thì `insufficient-permission`.

**Quyền cần có:** khoá có ít nhất một trong các quyền `pos.inventory.receipt`, `pos.inventory.adjust` và đủ các quyền `pos.inventory.read`, `pos.products.read`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Ví dụ yêu cầu**

```bash
curl -X POST "https://danix.vn/api/open/v1/inventory/documents/{id}/post" \
  -H "Authorization: Bearer dnx_live_…"
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (200)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000001201",
  "kind": "receipt",
  "code": "PN0001",
  "status": "posted",
  "warehouseId": "018f3b8e-1c2d-7a4b-9c3d-000000000001",
  "supplierId": null,
  "occurredAt": "2026-10-02T03:15:00.000Z",
  "postedAt": "2026-10-02T03:16:00.000Z",
  "costingMethod": "average",
  "note": null,
  "lines": []
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã phiếu. |
| `kind` | string | có | Loại phiếu: `receipt`, `issue`, `stocktake`… |
| `code` | string | có | Mã phiếu. |
| `status` | "draft" \| "ordered" \| "posted" \| "in_transit" \| "completed" \| "cancelled" | có | Trạng thái phiếu: `draft` là nháp, `posted` đã ghi sổ. |
| `warehouseId` | string (uuid) | có | Kho của phiếu. |
| `supplierId` | string (uuid) hoặc null | có | Nhà cung cấp. |
| `occurredAt` | string (date-time) | có | Ngày của phiếu. ISO 8601, múi giờ UTC. |
| `postedAt` | string (date-time) hoặc null | có | Thời điểm ghi sổ. ISO 8601 UTC hoặc `null`. |
| `costingMethod` | "average" \| "fifo" hoặc null | có | Phương pháp tính giá vốn khi ghi sổ. |
| `note` | string hoặc null | có | Ghi chú phiếu. |
| `lines` | array<object> | có | Các dòng của phiếu. |
| `lines[].id` | string (uuid) | có | Mã dòng phiếu. |
| `lines[].variantId` | string (uuid) | có | Mã mẫu mã. |
| `lines[].variantCode` | string | có | Mã mẫu mã. |
| `lines[].quantity` | string hoặc null | có | Số lượng nhập hoặc xuất. Chuỗi thập phân. |
| `lines[].countedQuantity` | string hoặc null | có | Số đếm thực tế (phiếu kiểm kho). |
| `lines[].unitCost` | string hoặc null | không | Giá nhập. Chỉ có khi khoá được xem giá vốn. |
| `lines[].note` | string hoặc null | có | Ghi chú dòng. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 413 | [`payload-too-large`](/developers/errors#payload-too-large) | Thân yêu cầu vượt quá dung lượng cho phép |
| 415 | [`unsupported-media-type`](/developers/errors#unsupported-media-type) | Thân yêu cầu phải là JSON (`Content-Type: application/json`) |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |
| 409 | [`conflict`](/developers/errors#conflict) | Thao tác xung đột với trạng thái hiện tại |
| 409 | [`document-already-posted`](/developers/errors#document-already-posted) | Phiếu kho đã ghi sổ |
| 422 | [`insufficient-stock`](/developers/errors#insufficient-stock) | Không đủ tồn kho cho thao tác này |
| 422 | [`variant-removed`](/developers/errors#variant-removed) | Mẫu mã đã bị gỡ khỏi sản phẩm |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

---

Nguồn: https://danix.vn/developers/reference/customers.md

# Tham chiếu: Khách hàng

Khách hàng, địa chỉ và ghi chú. Mọi đường dẫn dưới `https://danix.vn/api/open/v1`. Đặc tả máy đọc: [openapi.json](/openapi.json).

## Liệt kê khách hàng {#listCustomers}

`GET /customers`

Phân trang theo con trỏ. Lọc theo số điện thoại bằng `phone`.

**Quyền cần có:** khoá có quyền `pos.customers.read`.

**Tham số query**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `limit` | integer | không | Số dòng mỗi trang, từ 1 đến 100. Mặc định 50. |
| `cursor` | string | không | Con trỏ lấy từ `nextCursor` của trang trước, để lật trang trong CÙNG một lượt đọc (giữ nguyên các tham số khác). Không tự dựng, không lưu để nối lượt đồng bộ sau. |
| `updatedSince` | string (date-time) | không | Chỉ lấy bản ghi có `updatedAt` từ thời điểm này (ISO 8601 UTC), tính cả mốc. Đây là cách đồng bộ tăng dần: có tham số này thì danh sách chỉ trả bản ghi đã qua khoảng trễ an toàn (trần thời gian tua lại kho của shop cộng 15 giây, mặc định 2 phút 15 giây). |
| `includeDeleted` | "true" \| "false" | không | `true` để lấy cả bản ghi đã xoá (có `deletedAt`). Mặc định `false`. |
| `phone` | string | không | Tìm đúng số điện thoại (chấp nhận 0xxxxxxxxx hoặc +84…). |
| `search` | string | không | Tìm theo tên. |

**Ví dụ yêu cầu**

```bash
curl -X GET "https://danix.vn/api/open/v1/customers" \
  -H "Authorization: Bearer dnx_live_…"
```

**Phản hồi mẫu (200)**

```json
{
  "data": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000000201",
      "name": "Nguyễn Văn An",
      "gender": "male",
      "dateOfBirth": null,
      "source": "order",
      "isBlocked": false,
      "primaryPhone": "0901234567",
      "primaryEmail": null,
      "createdAt": "2026-10-02T03:15:00.000Z",
      "updatedAt": "2026-10-02T03:15:00.000Z",
      "deletedAt": null
    }
  ],
  "nextCursor": null
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `data` | array<object> | có | Các bản ghi của trang này. |
| `data[].id` | string (uuid) | có | Mã khách hàng. |
| `data[].name` | string | có | Tên khách. |
| `data[].gender` | "male" \| "female" \| "other" hoặc null | có | Giới tính. |
| `data[].dateOfBirth` | string hoặc null | có | Ngày sinh, dạng YYYY-MM-DD. |
| `data[].source` | "manual" \| "order" \| "import" | có | Nguồn tạo hồ sơ: `manual`, `order` hoặc `import`. |
| `data[].isBlocked` | boolean | có | Khách đang bị chặn. |
| `data[].primaryPhone` | string hoặc null | có | Số điện thoại chính. |
| `data[].primaryEmail` | string hoặc null | có | Email chính. |
| `data[].createdAt` | string (date-time) | có | Thời điểm tạo. ISO 8601, múi giờ UTC. |
| `data[].updatedAt` | string (date-time) | có | Thời điểm sửa gần nhất. ISO 8601, múi giờ UTC. |
| `data[].deletedAt` | string (date-time) hoặc null | có | Thời điểm xoá mềm; `null` nếu chưa xoá. ISO 8601 UTC hoặc `null`. |
| `nextCursor` | string hoặc null | có | Con trỏ của trang kế; `null` khi đã hết dữ liệu. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 400 | [`invalid-cursor`](/developers/errors#invalid-cursor) | Con trỏ phân trang không hợp lệ |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Tạo khách hàng {#createCustomer}

`POST /customers`

Tạo hồ sơ khách. Gửi `Idempotency-Key` để thử lại an toàn.

**Quyền cần có:** khoá có quyền `pos.customers.create`.

**Header**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | không | Khoá chống trùng do client đặt (khuyến nghị UUID). Gửi lại cùng khoá trong 24 giờ trả đúng kết quả lần đầu và header `Idempotent-Replayed: true`. |

**Thân yêu cầu (JSON)**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `name` | string | có | Tên khách. |
| `gender` | "male" \| "female" \| "other" hoặc null | không | Giới tính. |
| `dateOfBirth` | string hoặc null | không | Ngày sinh, dạng YYYY-MM-DD. |
| `contacts` | array<một trong nhiều dạng> | không | Số điện thoại và email. |
| `contacts[].kind` | "phone" | có |  |
| `contacts[].value` | string | có | Số điện thoại. |
| `contacts[].isPrimary` | boolean | không | Đặt làm số chính. |

**Ví dụ yêu cầu**

```bash
curl -X POST "https://danix.vn/api/open/v1/customers" \
  -H "Authorization: Bearer dnx_live_…" \
  -H "Idempotency-Key: 865d6735-58e7-488f-9ba7-be2de4de5651" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Nguyễn Văn An",
  "contacts": [
    {
      "kind": "phone",
      "value": "0901234567",
      "isPrimary": true
    }
  ]
}'
```

**Phản hồi mẫu (201)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000000201",
  "name": "Nguyễn Văn An",
  "gender": "male",
  "dateOfBirth": null,
  "source": "order",
  "isBlocked": false,
  "primaryPhone": "0901234567",
  "primaryEmail": null,
  "createdAt": "2026-10-02T03:15:00.000Z",
  "updatedAt": "2026-10-02T03:15:00.000Z",
  "deletedAt": null,
  "contacts": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000000901",
      "kind": "phone",
      "value": "0901234567",
      "isPrimary": true
    }
  ],
  "addresses": []
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã khách hàng. |
| `name` | string | có | Tên khách. |
| `gender` | "male" \| "female" \| "other" hoặc null | có | Giới tính. |
| `dateOfBirth` | string hoặc null | có | Ngày sinh, dạng YYYY-MM-DD. |
| `source` | "manual" \| "order" \| "import" | có | Nguồn tạo hồ sơ: `manual`, `order` hoặc `import`. |
| `isBlocked` | boolean | có | Khách đang bị chặn. |
| `primaryPhone` | string hoặc null | có | Số điện thoại chính. |
| `primaryEmail` | string hoặc null | có | Email chính. |
| `createdAt` | string (date-time) | có | Thời điểm tạo. ISO 8601, múi giờ UTC. |
| `updatedAt` | string (date-time) | có | Thời điểm sửa gần nhất. ISO 8601, múi giờ UTC. |
| `deletedAt` | string (date-time) hoặc null | có | Thời điểm xoá mềm; `null` nếu chưa xoá. ISO 8601 UTC hoặc `null`. |
| `contacts` | array<object> | có | Số điện thoại và email. |
| `contacts[].id` | string (uuid) | có | Mã liên hệ. |
| `contacts[].kind` | "phone" \| "email" | có | Loại liên hệ. |
| `contacts[].value` | string | có | Giá trị đã chuẩn hoá (số điện thoại 0xxxxxxxxx hoặc email). |
| `contacts[].isPrimary` | boolean | có | Liên hệ chính của loại này. |
| `addresses` | array<object> | có | Địa chỉ giao hàng, địa chỉ dùng gần nhất đứng đầu. |
| `addresses[].id` | string (uuid) | có | Mã địa chỉ. |
| `addresses[].recipientName` | string hoặc null | có | Tên người nhận. |
| `addresses[].recipientPhone` | string hoặc null | có | Số điện thoại người nhận. |
| `addresses[].addressLine` | string | có | Số nhà, đường. |
| `addresses[].geoSystem` | "old" \| "new" hoặc null | có | Hệ địa giới của các mã bên dưới: `old` (tỉnh, huyện, xã) hoặc `new` (tỉnh, xã). `null` khi địa chỉ chỉ có chữ. |
| `addresses[].provinceUnitId` | string (uuid) hoặc null | có | Mã (UUID) tỉnh hoặc thành phố; `null` khi không có. |
| `addresses[].districtUnitId` | string (uuid) hoặc null | có | Mã (UUID) quận hoặc huyện; hệ `new` luôn `null`. |
| `addresses[].wardUnitId` | string (uuid) hoặc null | có | Mã (UUID) phường hoặc xã; `null` khi không có. |
| `addresses[].provinceName` | string hoặc null | có | Tỉnh hoặc thành phố. |
| `addresses[].districtName` | string hoặc null | có | Quận hoặc huyện. |
| `addresses[].wardName` | string hoặc null | có | Phường hoặc xã. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 413 | [`payload-too-large`](/developers/errors#payload-too-large) | Thân yêu cầu vượt quá dung lượng cho phép |
| 415 | [`unsupported-media-type`](/developers/errors#unsupported-media-type) | Thân yêu cầu phải là JSON (`Content-Type: application/json`) |
| 409 | [`conflict`](/developers/errors#conflict) | Thao tác xung đột với trạng thái hiện tại |
| 409 | [`idempotency-key-in-progress`](/developers/errors#idempotency-key-in-progress) | Yêu cầu với cùng Idempotency-Key đang được xử lý |
| 422 | [`idempotency-key-reused`](/developers/errors#idempotency-key-reused) | Idempotency-Key đã dùng cho một yêu cầu có nội dung khác |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Chi tiết một khách hàng {#getCustomer}

`GET /customers/{id}`

Trả khách kèm số điện thoại, email và địa chỉ giao hàng.

**Quyền cần có:** khoá có quyền `pos.customers.read`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Ví dụ yêu cầu**

```bash
curl -X GET "https://danix.vn/api/open/v1/customers/{id}" \
  -H "Authorization: Bearer dnx_live_…"
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (200)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000000201",
  "name": "Nguyễn Văn An",
  "gender": "male",
  "dateOfBirth": null,
  "source": "order",
  "isBlocked": false,
  "primaryPhone": "0901234567",
  "primaryEmail": null,
  "createdAt": "2026-10-02T03:15:00.000Z",
  "updatedAt": "2026-10-02T03:15:00.000Z",
  "deletedAt": null,
  "contacts": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000000901",
      "kind": "phone",
      "value": "0901234567",
      "isPrimary": true
    }
  ],
  "addresses": []
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã khách hàng. |
| `name` | string | có | Tên khách. |
| `gender` | "male" \| "female" \| "other" hoặc null | có | Giới tính. |
| `dateOfBirth` | string hoặc null | có | Ngày sinh, dạng YYYY-MM-DD. |
| `source` | "manual" \| "order" \| "import" | có | Nguồn tạo hồ sơ: `manual`, `order` hoặc `import`. |
| `isBlocked` | boolean | có | Khách đang bị chặn. |
| `primaryPhone` | string hoặc null | có | Số điện thoại chính. |
| `primaryEmail` | string hoặc null | có | Email chính. |
| `createdAt` | string (date-time) | có | Thời điểm tạo. ISO 8601, múi giờ UTC. |
| `updatedAt` | string (date-time) | có | Thời điểm sửa gần nhất. ISO 8601, múi giờ UTC. |
| `deletedAt` | string (date-time) hoặc null | có | Thời điểm xoá mềm; `null` nếu chưa xoá. ISO 8601 UTC hoặc `null`. |
| `contacts` | array<object> | có | Số điện thoại và email. |
| `contacts[].id` | string (uuid) | có | Mã liên hệ. |
| `contacts[].kind` | "phone" \| "email" | có | Loại liên hệ. |
| `contacts[].value` | string | có | Giá trị đã chuẩn hoá (số điện thoại 0xxxxxxxxx hoặc email). |
| `contacts[].isPrimary` | boolean | có | Liên hệ chính của loại này. |
| `addresses` | array<object> | có | Địa chỉ giao hàng, địa chỉ dùng gần nhất đứng đầu. |
| `addresses[].id` | string (uuid) | có | Mã địa chỉ. |
| `addresses[].recipientName` | string hoặc null | có | Tên người nhận. |
| `addresses[].recipientPhone` | string hoặc null | có | Số điện thoại người nhận. |
| `addresses[].addressLine` | string | có | Số nhà, đường. |
| `addresses[].geoSystem` | "old" \| "new" hoặc null | có | Hệ địa giới của các mã bên dưới: `old` (tỉnh, huyện, xã) hoặc `new` (tỉnh, xã). `null` khi địa chỉ chỉ có chữ. |
| `addresses[].provinceUnitId` | string (uuid) hoặc null | có | Mã (UUID) tỉnh hoặc thành phố; `null` khi không có. |
| `addresses[].districtUnitId` | string (uuid) hoặc null | có | Mã (UUID) quận hoặc huyện; hệ `new` luôn `null`. |
| `addresses[].wardUnitId` | string (uuid) hoặc null | có | Mã (UUID) phường hoặc xã; `null` khi không có. |
| `addresses[].provinceName` | string hoặc null | có | Tỉnh hoặc thành phố. |
| `addresses[].districtName` | string hoặc null | có | Quận hoặc huyện. |
| `addresses[].wardName` | string hoặc null | có | Phường hoặc xã. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Sửa khách hàng {#updateCustomer}

`PATCH /customers/{id}`

Chỉ gửi trường cần đổi. Số điện thoại và email không sửa được qua đường này.

**Quyền cần có:** khoá có quyền `pos.customers.update`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Thân yêu cầu (JSON)**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `name` | string | không | Tên khách. |
| `gender` | "male" \| "female" \| "other" hoặc null | không | Giới tính. |
| `dateOfBirth` | string hoặc null | không | Ngày sinh, dạng YYYY-MM-DD. |

**Ví dụ yêu cầu**

```bash
curl -X PATCH "https://danix.vn/api/open/v1/customers/{id}" \
  -H "Authorization: Bearer dnx_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Nguyễn Văn An (VIP)"
}'
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (200)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000000201",
  "name": "Nguyễn Văn An",
  "gender": "male",
  "dateOfBirth": null,
  "source": "order",
  "isBlocked": false,
  "primaryPhone": "0901234567",
  "primaryEmail": null,
  "createdAt": "2026-10-02T03:15:00.000Z",
  "updatedAt": "2026-10-02T03:15:00.000Z",
  "deletedAt": null,
  "contacts": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000000901",
      "kind": "phone",
      "value": "0901234567",
      "isPrimary": true
    }
  ],
  "addresses": []
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã khách hàng. |
| `name` | string | có | Tên khách. |
| `gender` | "male" \| "female" \| "other" hoặc null | có | Giới tính. |
| `dateOfBirth` | string hoặc null | có | Ngày sinh, dạng YYYY-MM-DD. |
| `source` | "manual" \| "order" \| "import" | có | Nguồn tạo hồ sơ: `manual`, `order` hoặc `import`. |
| `isBlocked` | boolean | có | Khách đang bị chặn. |
| `primaryPhone` | string hoặc null | có | Số điện thoại chính. |
| `primaryEmail` | string hoặc null | có | Email chính. |
| `createdAt` | string (date-time) | có | Thời điểm tạo. ISO 8601, múi giờ UTC. |
| `updatedAt` | string (date-time) | có | Thời điểm sửa gần nhất. ISO 8601, múi giờ UTC. |
| `deletedAt` | string (date-time) hoặc null | có | Thời điểm xoá mềm; `null` nếu chưa xoá. ISO 8601 UTC hoặc `null`. |
| `contacts` | array<object> | có | Số điện thoại và email. |
| `contacts[].id` | string (uuid) | có | Mã liên hệ. |
| `contacts[].kind` | "phone" \| "email" | có | Loại liên hệ. |
| `contacts[].value` | string | có | Giá trị đã chuẩn hoá (số điện thoại 0xxxxxxxxx hoặc email). |
| `contacts[].isPrimary` | boolean | có | Liên hệ chính của loại này. |
| `addresses` | array<object> | có | Địa chỉ giao hàng, địa chỉ dùng gần nhất đứng đầu. |
| `addresses[].id` | string (uuid) | có | Mã địa chỉ. |
| `addresses[].recipientName` | string hoặc null | có | Tên người nhận. |
| `addresses[].recipientPhone` | string hoặc null | có | Số điện thoại người nhận. |
| `addresses[].addressLine` | string | có | Số nhà, đường. |
| `addresses[].geoSystem` | "old" \| "new" hoặc null | có | Hệ địa giới của các mã bên dưới: `old` (tỉnh, huyện, xã) hoặc `new` (tỉnh, xã). `null` khi địa chỉ chỉ có chữ. |
| `addresses[].provinceUnitId` | string (uuid) hoặc null | có | Mã (UUID) tỉnh hoặc thành phố; `null` khi không có. |
| `addresses[].districtUnitId` | string (uuid) hoặc null | có | Mã (UUID) quận hoặc huyện; hệ `new` luôn `null`. |
| `addresses[].wardUnitId` | string (uuid) hoặc null | có | Mã (UUID) phường hoặc xã; `null` khi không có. |
| `addresses[].provinceName` | string hoặc null | có | Tỉnh hoặc thành phố. |
| `addresses[].districtName` | string hoặc null | có | Quận hoặc huyện. |
| `addresses[].wardName` | string hoặc null | có | Phường hoặc xã. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 413 | [`payload-too-large`](/developers/errors#payload-too-large) | Thân yêu cầu vượt quá dung lượng cho phép |
| 415 | [`unsupported-media-type`](/developers/errors#unsupported-media-type) | Thân yêu cầu phải là JSON (`Content-Type: application/json`) |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Thêm địa chỉ cho khách {#createCustomerAddress}

`POST /customers/{id}/addresses`

Thêm một địa chỉ giao hàng vào hồ sơ khách. Khách đã có một địa chỉ đúng như vậy thì trả `conflict`. Gửi mã đơn vị hành chính (tra bằng `GET /geo/units`) để đơn dùng địa chỉ này đẩy được sang hãng vận chuyển.

**Quyền cần có:** khoá có quyền `pos.customers.update`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Thân yêu cầu (JSON)**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `recipientName` | string hoặc null | không | Tên người nhận. |
| `recipientPhone` | string hoặc null | không | Số điện thoại người nhận. |
| `addressLine` | string | có | Số nhà, đường. |
| `provinceName` | string hoặc null | không | Tỉnh hoặc thành phố. |
| `districtName` | string hoặc null | không | Quận hoặc huyện. |
| `wardName` | string hoặc null | không | Phường hoặc xã. |
| `geoSystem` | "old" \| "new" hoặc null | không | Hệ địa giới của các mã bên dưới: `old` (tỉnh, huyện, xã) hoặc `new` (tỉnh, xã). Bỏ trống thì suy từ mã. |
| `provinceUnitId` | string (uuid) hoặc null | không | Mã (UUID) tỉnh hoặc thành phố trong danh mục hành chính của DANIX. |
| `districtUnitId` | string (uuid) hoặc null | không | Mã (UUID) quận hoặc huyện. Hệ `new` không có cấp này. |
| `wardUnitId` | string (uuid) hoặc null | không | Mã (UUID) phường hoặc xã. |

**Ví dụ yêu cầu**

```bash
curl -X POST "https://danix.vn/api/open/v1/customers/{id}/addresses" \
  -H "Authorization: Bearer dnx_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "addressLine": "12 Lê Lợi",
  "provinceName": "Đà Nẵng"
}'
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (201)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000000999",
  "recipientName": null,
  "recipientPhone": null,
  "addressLine": "12 Lê Lợi, Đà Nẵng",
  "geoSystem": null,
  "provinceUnitId": null,
  "districtUnitId": null,
  "wardUnitId": null,
  "provinceName": null,
  "districtName": null,
  "wardName": null
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã địa chỉ. |
| `recipientName` | string hoặc null | có | Tên người nhận. |
| `recipientPhone` | string hoặc null | có | Số điện thoại người nhận. |
| `addressLine` | string | có | Số nhà, đường. |
| `geoSystem` | "old" \| "new" hoặc null | có | Hệ địa giới của các mã bên dưới: `old` (tỉnh, huyện, xã) hoặc `new` (tỉnh, xã). `null` khi địa chỉ chỉ có chữ. |
| `provinceUnitId` | string (uuid) hoặc null | có | Mã (UUID) tỉnh hoặc thành phố; `null` khi không có. |
| `districtUnitId` | string (uuid) hoặc null | có | Mã (UUID) quận hoặc huyện; hệ `new` luôn `null`. |
| `wardUnitId` | string (uuid) hoặc null | có | Mã (UUID) phường hoặc xã; `null` khi không có. |
| `provinceName` | string hoặc null | có | Tỉnh hoặc thành phố. |
| `districtName` | string hoặc null | có | Quận hoặc huyện. |
| `wardName` | string hoặc null | có | Phường hoặc xã. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 413 | [`payload-too-large`](/developers/errors#payload-too-large) | Thân yêu cầu vượt quá dung lượng cho phép |
| 415 | [`unsupported-media-type`](/developers/errors#unsupported-media-type) | Thân yêu cầu phải là JSON (`Content-Type: application/json`) |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |
| 409 | [`conflict`](/developers/errors#conflict) | Thao tác xung đột với trạng thái hiện tại |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Sửa địa chỉ của khách {#updateCustomerAddress}

`PATCH /customers/{id}/addresses/{addressId}`

Thay nội dung một địa chỉ giao hàng; trùng đúng một địa chỉ khác của khách thì trả `conflict`. Gửi tên địa danh (tỉnh, huyện, xã) mà không gửi mã thì mã đơn vị cũ bị XOÁ — địa chỉ không còn đẩy hãng được tới khi gửi lại mã; không gửi trường địa danh nào thì giữ nguyên.

**Quyền cần có:** khoá có quyền `pos.customers.update`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |
| `addressId` | string (uuid) | có | Mã địa chỉ. |

**Thân yêu cầu (JSON)**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `recipientName` | string hoặc null | không | Tên người nhận. |
| `recipientPhone` | string hoặc null | không | Số điện thoại người nhận. |
| `addressLine` | string | có | Số nhà, đường. |
| `provinceName` | string hoặc null | không | Tỉnh hoặc thành phố. |
| `districtName` | string hoặc null | không | Quận hoặc huyện. |
| `wardName` | string hoặc null | không | Phường hoặc xã. |
| `geoSystem` | "old" \| "new" hoặc null | không | Hệ địa giới của các mã bên dưới: `old` (tỉnh, huyện, xã) hoặc `new` (tỉnh, xã). Bỏ trống thì suy từ mã. |
| `provinceUnitId` | string (uuid) hoặc null | không | Mã (UUID) tỉnh hoặc thành phố trong danh mục hành chính của DANIX. |
| `districtUnitId` | string (uuid) hoặc null | không | Mã (UUID) quận hoặc huyện. Hệ `new` không có cấp này. |
| `wardUnitId` | string (uuid) hoặc null | không | Mã (UUID) phường hoặc xã. |

**Ví dụ yêu cầu**

```bash
curl -X PATCH "https://danix.vn/api/open/v1/customers/{id}/addresses/{addressId}" \
  -H "Authorization: Bearer dnx_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "addressLine": "34 Trần Phú"
}'
```

Thay `{id}`, `{addressId}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (200)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000000999",
  "recipientName": null,
  "recipientPhone": null,
  "addressLine": "34 Trần Phú",
  "geoSystem": "old",
  "provinceUnitId": "018f3b8e-1c2d-7a4b-9c3d-000000001501",
  "districtUnitId": "018f3b8e-1c2d-7a4b-9c3d-000000001502",
  "wardUnitId": "018f3b8e-1c2d-7a4b-9c3d-000000001503",
  "provinceName": "Đà Nẵng",
  "districtName": "Hải Châu",
  "wardName": "Thạch Thang"
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã địa chỉ. |
| `recipientName` | string hoặc null | có | Tên người nhận. |
| `recipientPhone` | string hoặc null | có | Số điện thoại người nhận. |
| `addressLine` | string | có | Số nhà, đường. |
| `geoSystem` | "old" \| "new" hoặc null | có | Hệ địa giới của các mã bên dưới: `old` (tỉnh, huyện, xã) hoặc `new` (tỉnh, xã). `null` khi địa chỉ chỉ có chữ. |
| `provinceUnitId` | string (uuid) hoặc null | có | Mã (UUID) tỉnh hoặc thành phố; `null` khi không có. |
| `districtUnitId` | string (uuid) hoặc null | có | Mã (UUID) quận hoặc huyện; hệ `new` luôn `null`. |
| `wardUnitId` | string (uuid) hoặc null | có | Mã (UUID) phường hoặc xã; `null` khi không có. |
| `provinceName` | string hoặc null | có | Tỉnh hoặc thành phố. |
| `districtName` | string hoặc null | có | Quận hoặc huyện. |
| `wardName` | string hoặc null | có | Phường hoặc xã. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 413 | [`payload-too-large`](/developers/errors#payload-too-large) | Thân yêu cầu vượt quá dung lượng cho phép |
| 415 | [`unsupported-media-type`](/developers/errors#unsupported-media-type) | Thân yêu cầu phải là JSON (`Content-Type: application/json`) |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |
| 409 | [`conflict`](/developers/errors#conflict) | Thao tác xung đột với trạng thái hiện tại |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Xoá địa chỉ của khách {#deleteCustomerAddress}

`DELETE /customers/{id}/addresses/{addressId}`

Xoá một địa chỉ giao hàng khỏi hồ sơ khách. Đơn cũ vẫn giữ địa chỉ đã ghi.

**Quyền cần có:** khoá có quyền `pos.customers.delete`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |
| `addressId` | string (uuid) | có | Mã địa chỉ. |

**Ví dụ yêu cầu**

```bash
curl -X DELETE "https://danix.vn/api/open/v1/customers/{id}/addresses/{addressId}" \
  -H "Authorization: Bearer dnx_live_…"
```

Thay `{id}`, `{addressId}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (200)**

```json
{
  "ok": true
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `ok` | true | có | Luôn `true` khi thao tác thành công. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Ghi chú của khách {#listCustomerNotes}

`GET /customers/{id}/notes`

Danh sách ghi chú, mới nhất trước.

**Quyền cần có:** khoá có quyền `pos.customers.read`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Ví dụ yêu cầu**

```bash
curl -X GET "https://danix.vn/api/open/v1/customers/{id}/notes" \
  -H "Authorization: Bearer dnx_live_…"
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (200)**

```json
{
  "data": []
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `data` | array<object> | có | Các ghi chú, mới nhất trước. |
| `data[].id` | string (uuid) | có | Mã ghi chú. |
| `data[].message` | string | có | Nội dung ghi chú. |
| `data[].createdByName` | string hoặc null | có | Tên người hay ứng dụng đã ghi. |
| `data[].createdByIsIntegration` | boolean | có | `true` khi ghi chú do một ứng dụng kết nối tạo. |
| `data[].createdAt` | string (date-time) | có | Thời điểm tạo. ISO 8601, múi giờ UTC. |
| `data[].updatedAt` | string (date-time) | có | Thời điểm sửa gần nhất. ISO 8601, múi giờ UTC. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Thêm ghi chú vào khách {#createCustomerNote}

`POST /customers/{id}/notes`

Ghi chú đứng tên ứng dụng kết nối.

**Quyền cần có:** khoá có quyền `pos.customers.update`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Thân yêu cầu (JSON)**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `message` | string | có | Nội dung ghi chú. |

**Ví dụ yêu cầu**

```bash
curl -X POST "https://danix.vn/api/open/v1/customers/{id}/notes" \
  -H "Authorization: Bearer dnx_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "message": "Khách thích giao buổi tối"
}'
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (201)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000000778",
  "message": "Khách thích giao buổi tối",
  "createdByName": "Đồng bộ kế toán",
  "createdByIsIntegration": true,
  "createdAt": "2026-10-02T03:15:00.000Z",
  "updatedAt": "2026-10-02T03:15:00.000Z"
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã ghi chú. |
| `message` | string | có | Nội dung ghi chú. |
| `createdByName` | string hoặc null | có | Tên người hay ứng dụng đã ghi. |
| `createdByIsIntegration` | boolean | có | `true` khi ghi chú do một ứng dụng kết nối tạo. |
| `createdAt` | string (date-time) | có | Thời điểm tạo. ISO 8601, múi giờ UTC. |
| `updatedAt` | string (date-time) | có | Thời điểm sửa gần nhất. ISO 8601, múi giờ UTC. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 413 | [`payload-too-large`](/developers/errors#payload-too-large) | Thân yêu cầu vượt quá dung lượng cho phép |
| 415 | [`unsupported-media-type`](/developers/errors#unsupported-media-type) | Thân yêu cầu phải là JSON (`Content-Type: application/json`) |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

---

Nguồn: https://danix.vn/developers/reference/geo.md

# Tham chiếu: Địa giới hành chính

Danh mục tỉnh, huyện, xã để lấy mã đơn vị cho địa chỉ giao hàng. Mọi đường dẫn dưới `https://danix.vn/api/open/v1`. Đặc tả máy đọc: [openapi.json](/openapi.json).

## Tra danh mục đơn vị hành chính {#listGeoUnits}

`GET /geo/units`

Duyệt cây tỉnh → (huyện) → xã của một hệ địa giới, mỗi lượt một tầng: vắng `parentId` là các tỉnh, có `parentId` là các đơn vị con của nó. Không phân trang (mỗi tầng tối đa vài trăm dòng), sắp theo tên. Dùng `id` của kết quả làm `provinceUnitId`, `districtUnitId`, `wardUnitId` khi tạo đơn hay thêm địa chỉ khách — có mã thì đơn đẩy được sang hãng vận chuyển. `parentId` không có hoặc khác hệ trả `not-found`.

**Quyền cần có:** không cần quyền nào ngoài khoá hợp lệ.

**Tham số query**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `system` | "old" \| "new" | có | Hệ địa giới cần tra: `old` hoặc `new`. Bắt buộc. |
| `parentId` | string (uuid) | không | Mã đơn vị cha. Vắng thì trả các tỉnh, thành phố của hệ. |
| `search` | string | không | Lọc theo tên, không phân biệt hoa thường và dấu, trong tầng đang tra. |

**Ví dụ yêu cầu**

```bash
curl -X GET "https://danix.vn/api/open/v1/geo/units?system=old" \
  -H "Authorization: Bearer dnx_live_…"
```

**Phản hồi mẫu (200)**

```json
{
  "data": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000001501",
      "system": "old",
      "level": 1,
      "parentId": null,
      "name": "Đà Nẵng"
    }
  ]
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `data` | array<object> | có | Các đơn vị của tầng. |
| `data[].id` | string (uuid) | có | Mã (UUID) của đơn vị trong danh mục của DANIX. |
| `data[].system` | "old" \| "new" | có | Hệ địa giới: `old` (tỉnh, huyện, xã — trước 01/07/2025) hoặc `new` (tỉnh, xã). |
| `data[].level` | integer | có | Cấp: 1 tỉnh hoặc thành phố, 2 quận hoặc huyện (chỉ hệ `old`), 3 phường hoặc xã. |
| `data[].parentId` | string (uuid) hoặc null | có | Đơn vị cấp trên; `null` với cấp tỉnh. |
| `data[].name` | string | có | Tên đơn vị. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

---

Nguồn: https://danix.vn/developers/reference/shipments.md

# Tham chiếu: Vận đơn

Vận đơn, hành trình và huỷ vận đơn. Mọi đường dẫn dưới `https://danix.vn/api/open/v1`. Đặc tả máy đọc: [openapi.json](/openapi.json).

## Liệt kê vận đơn {#listShipments}

`GET /shipments`

Phân trang theo con trỏ. Dòng danh sách không có hành trình; lấy bằng `GET /shipments/{id}`.

**Quyền cần có:** khoá có quyền `pos.shipping.read` và quyền `pos.orders.read`.

**Tham số query**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `limit` | integer | không | Số dòng mỗi trang, từ 1 đến 100. Mặc định 50. |
| `cursor` | string | không | Con trỏ lấy từ `nextCursor` của trang trước, để lật trang trong CÙNG một lượt đọc (giữ nguyên các tham số khác). Không tự dựng, không lưu để nối lượt đồng bộ sau. |
| `updatedSince` | string (date-time) | không | Chỉ lấy bản ghi có `updatedAt` từ thời điểm này (ISO 8601 UTC), tính cả mốc. Đây là cách đồng bộ tăng dần: có tham số này thì danh sách chỉ trả bản ghi đã qua khoảng trễ an toàn (trần thời gian tua lại kho của shop cộng 15 giây, mặc định 2 phút 15 giây). |
| `status` | "pending" \| "delivering" \| "delivered" \| "delivery_failed" \| "returning" \| "returned" \| "partially_returned" \| "cancelled" \| "lost" \| "damaged" \| "exception" \| "scrapped" | không | Chỉ lấy vận đơn ở trạng thái này. |
| `orderId` | string (uuid) | không | Chỉ lấy vận đơn của đơn này. |

**Ví dụ yêu cầu**

```bash
curl -X GET "https://danix.vn/api/open/v1/shipments" \
  -H "Authorization: Bearer dnx_live_…"
```

**Phản hồi mẫu (200)**

```json
{
  "data": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000001001",
      "carrier": "ghn",
      "trackingCode": "GHNABC123",
      "status": "delivering",
      "orderId": "018f3b8e-1c2d-7a4b-9c3d-000000000101",
      "orderCode": "DH1024",
      "codAmount": "480000",
      "feeTotal": "30000",
      "createdAt": "2026-10-02T03:15:00.000Z",
      "expectedDeliveryAt": null,
      "failReason": null,
      "updatedAt": "2026-10-02T03:15:00.000Z"
    }
  ],
  "nextCursor": null
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `data` | array<object> | có | Các bản ghi của trang này. |
| `data[].id` | string (uuid) | có | Mã vận đơn. |
| `data[].carrier` | "ghn" \| "vtp" | có | Hãng vận chuyển. |
| `data[].trackingCode` | string hoặc null | có | Mã vận đơn của hãng. Rỗng khi chưa đẩy sang hãng. |
| `data[].status` | "pending" \| "delivering" \| "delivered" \| "delivery_failed" \| "returning" \| "returned" \| "partially_returned" \| "cancelled" \| "lost" \| "damaged" \| "exception" \| "scrapped" | có | Trạng thái chuẩn của vận đơn. |
| `data[].orderId` | string (uuid) | có | Đơn hàng của vận đơn. |
| `data[].orderCode` | string | có | Mã đơn hàng. |
| `data[].codAmount` | string hoặc null | có | Tiền thu hộ (COD). Chuỗi thập phân hoặc `null`. |
| `data[].feeTotal` | string hoặc null | có | Tổng phí vận chuyển. Chuỗi thập phân hoặc `null`. |
| `data[].createdAt` | string (date-time) | có | Thời điểm tạo. ISO 8601, múi giờ UTC. |
| `data[].updatedAt` | string (date-time) | có | Lần sửa gần nhất. Danh sách sắp theo `(updatedAt, id)`; dùng làm mốc `updatedSince`. ISO 8601, múi giờ UTC. |
| `data[].expectedDeliveryAt` | string (date-time) hoặc null | có | Dự kiến giao. ISO 8601 UTC hoặc `null`. |
| `data[].failReason` | string hoặc null | có | Lý do giao thất bại, nếu có. |
| `nextCursor` | string hoặc null | có | Con trỏ của trang kế; `null` khi đã hết dữ liệu. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 400 | [`invalid-cursor`](/developers/errors#invalid-cursor) | Con trỏ phân trang không hợp lệ |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Tạo vận đơn cho một đơn {#createShipment}

`POST /shipments`

Đẩy đơn sang hãng vận chuyển qua một kết nối đã cấu hình trong shop.

**Quyền cần có:** khoá có quyền `pos.shipping.read` và đủ các quyền `pos.orders.update`, `pos.products.read`.

**Thân yêu cầu (JSON)**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `orderId` | string (uuid) | có | Đơn hàng cần giao. |
| `connectionId` | string (uuid) | có | Kết nối hãng vận chuyển của shop dùng để giao. |
| `weightGram` | integer | không | Khối lượng khai với hãng, gram, từ 1 tới 50000. |
| `codAmount` | string | không | Tiền thu hộ. Mặc định theo đơn. |
| `note` | string | không | Ghi chú cho hãng. |

**Ví dụ yêu cầu**

```bash
curl -X POST "https://danix.vn/api/open/v1/shipments" \
  -H "Authorization: Bearer dnx_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "orderId": "018f3b8e-1c2d-7a4b-9c3d-000000000101",
  "connectionId": "018f3b8e-1c2d-7a4b-9c3d-000000001301"
}'
```

**Phản hồi mẫu (201)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000001001",
  "carrier": "ghn",
  "trackingCode": "GHNABC123",
  "status": "delivering",
  "orderId": "018f3b8e-1c2d-7a4b-9c3d-000000000101",
  "orderCode": "DH1024",
  "codAmount": "480000",
  "feeTotal": "30000",
  "createdAt": "2026-10-02T03:15:00.000Z",
  "expectedDeliveryAt": null,
  "failReason": null,
  "recipientName": "Nguyễn Văn An",
  "recipientPhone": "0901234567",
  "recipientAddress": "12 Lê Lợi, Thạch Thang, Hải Châu, Đà Nẵng",
  "weightGram": 500,
  "pickedUpAt": "2026-10-02T03:15:00.000Z",
  "events": []
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã vận đơn. |
| `carrier` | "ghn" \| "vtp" | có | Hãng vận chuyển. |
| `trackingCode` | string hoặc null | có | Mã vận đơn của hãng. Rỗng khi chưa đẩy sang hãng. |
| `status` | "pending" \| "delivering" \| "delivered" \| "delivery_failed" \| "returning" \| "returned" \| "partially_returned" \| "cancelled" \| "lost" \| "damaged" \| "exception" \| "scrapped" | có | Trạng thái chuẩn của vận đơn. |
| `orderId` | string (uuid) | có | Đơn hàng của vận đơn. |
| `orderCode` | string | có | Mã đơn hàng. |
| `codAmount` | string hoặc null | có | Tiền thu hộ (COD). Chuỗi thập phân hoặc `null`. |
| `feeTotal` | string hoặc null | có | Tổng phí vận chuyển. Chuỗi thập phân hoặc `null`. |
| `createdAt` | string (date-time) | có | Thời điểm tạo. ISO 8601, múi giờ UTC. |
| `expectedDeliveryAt` | string (date-time) hoặc null | có | Dự kiến giao. ISO 8601 UTC hoặc `null`. |
| `failReason` | string hoặc null | có | Lý do giao thất bại, nếu có. |
| `recipientName` | string hoặc null | có | Tên người nhận. |
| `recipientPhone` | string hoặc null | có | Số điện thoại người nhận. |
| `recipientAddress` | string hoặc null | có | Địa chỉ người nhận. |
| `weightGram` | integer hoặc null | có | Khối lượng khai với hãng, gram. |
| `pickedUpAt` | string (date-time) hoặc null | có | Thời điểm hãng lấy hàng. ISO 8601 UTC hoặc `null`. |
| `events` | array<object> | có | Hành trình, theo thứ tự thời gian. |
| `events[].id` | string (uuid) | có | Mã sự kiện. |
| `events[].status` | "pending" \| "delivering" \| "delivered" \| "delivery_failed" \| "returning" \| "returned" \| "partially_returned" \| "cancelled" \| "lost" \| "damaged" \| "exception" \| "scrapped" hoặc null | có | Trạng thái chuẩn sau sự kiện; `null` nếu sự kiện không đổi trạng thái. |
| `events[].carrierStatus` | string hoặc null | có | Mã trạng thái gốc của hãng. |
| `events[].occurredAt` | string (date-time) | có | Thời điểm hãng ghi nhận. ISO 8601, múi giờ UTC. |
| `events[].location` | string hoặc null | có | Nơi xảy ra. |
| `events[].description` | string hoặc null | có | Mô tả của hãng. |
| `events[].reason` | string hoặc null | có | Lý do (giao thất bại, hoàn…). |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 413 | [`payload-too-large`](/developers/errors#payload-too-large) | Thân yêu cầu vượt quá dung lượng cho phép |
| 415 | [`unsupported-media-type`](/developers/errors#unsupported-media-type) | Thân yêu cầu phải là JSON (`Content-Type: application/json`) |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |
| 409 | [`conflict`](/developers/errors#conflict) | Thao tác xung đột với trạng thái hiện tại |
| 422 | [`order-status-not-allowed`](/developers/errors#order-status-not-allowed) | Không chuyển được đơn sang trạng thái này từ trạng thái hiện tại |
| 422 | [`insufficient-stock`](/developers/errors#insufficient-stock) | Không đủ tồn kho cho thao tác này |
| 503 | [`carrier-unavailable`](/developers/errors#carrier-unavailable) | Hãng vận chuyển hiện không với tới được, hãy thử lại sau |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Chi tiết một vận đơn {#getShipment}

`GET /shipments/{id}`

Trả vận đơn kèm hành trình theo thứ tự thời gian.

**Quyền cần có:** khoá có quyền `pos.shipping.read` và quyền `pos.orders.read`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Ví dụ yêu cầu**

```bash
curl -X GET "https://danix.vn/api/open/v1/shipments/{id}" \
  -H "Authorization: Bearer dnx_live_…"
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (200)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000001001",
  "carrier": "ghn",
  "trackingCode": "GHNABC123",
  "status": "delivering",
  "orderId": "018f3b8e-1c2d-7a4b-9c3d-000000000101",
  "orderCode": "DH1024",
  "codAmount": "480000",
  "feeTotal": "30000",
  "createdAt": "2026-10-02T03:15:00.000Z",
  "expectedDeliveryAt": null,
  "failReason": null,
  "recipientName": "Nguyễn Văn An",
  "recipientPhone": "0901234567",
  "recipientAddress": "12 Lê Lợi, Thạch Thang, Hải Châu, Đà Nẵng",
  "weightGram": 500,
  "pickedUpAt": "2026-10-02T03:15:00.000Z",
  "events": []
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã vận đơn. |
| `carrier` | "ghn" \| "vtp" | có | Hãng vận chuyển. |
| `trackingCode` | string hoặc null | có | Mã vận đơn của hãng. Rỗng khi chưa đẩy sang hãng. |
| `status` | "pending" \| "delivering" \| "delivered" \| "delivery_failed" \| "returning" \| "returned" \| "partially_returned" \| "cancelled" \| "lost" \| "damaged" \| "exception" \| "scrapped" | có | Trạng thái chuẩn của vận đơn. |
| `orderId` | string (uuid) | có | Đơn hàng của vận đơn. |
| `orderCode` | string | có | Mã đơn hàng. |
| `codAmount` | string hoặc null | có | Tiền thu hộ (COD). Chuỗi thập phân hoặc `null`. |
| `feeTotal` | string hoặc null | có | Tổng phí vận chuyển. Chuỗi thập phân hoặc `null`. |
| `createdAt` | string (date-time) | có | Thời điểm tạo. ISO 8601, múi giờ UTC. |
| `expectedDeliveryAt` | string (date-time) hoặc null | có | Dự kiến giao. ISO 8601 UTC hoặc `null`. |
| `failReason` | string hoặc null | có | Lý do giao thất bại, nếu có. |
| `recipientName` | string hoặc null | có | Tên người nhận. |
| `recipientPhone` | string hoặc null | có | Số điện thoại người nhận. |
| `recipientAddress` | string hoặc null | có | Địa chỉ người nhận. |
| `weightGram` | integer hoặc null | có | Khối lượng khai với hãng, gram. |
| `pickedUpAt` | string (date-time) hoặc null | có | Thời điểm hãng lấy hàng. ISO 8601 UTC hoặc `null`. |
| `events` | array<object> | có | Hành trình, theo thứ tự thời gian. |
| `events[].id` | string (uuid) | có | Mã sự kiện. |
| `events[].status` | "pending" \| "delivering" \| "delivered" \| "delivery_failed" \| "returning" \| "returned" \| "partially_returned" \| "cancelled" \| "lost" \| "damaged" \| "exception" \| "scrapped" hoặc null | có | Trạng thái chuẩn sau sự kiện; `null` nếu sự kiện không đổi trạng thái. |
| `events[].carrierStatus` | string hoặc null | có | Mã trạng thái gốc của hãng. |
| `events[].occurredAt` | string (date-time) | có | Thời điểm hãng ghi nhận. ISO 8601, múi giờ UTC. |
| `events[].location` | string hoặc null | có | Nơi xảy ra. |
| `events[].description` | string hoặc null | có | Mô tả của hãng. |
| `events[].reason` | string hoặc null | có | Lý do (giao thất bại, hoàn…). |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Huỷ vận đơn {#cancelShipment}

`POST /shipments/{id}/cancel`

Huỷ vận đơn chưa kết thúc. Vận đơn đã giao hay đã hoàn không huỷ được.

**Quyền cần có:** khoá có quyền `pos.shipping.read` và quyền `pos.orders.update`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Thân yêu cầu (JSON)**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `reason` | string | không | Lý do huỷ, lưu vào nhật ký. |

**Ví dụ yêu cầu**

```bash
curl -X POST "https://danix.vn/api/open/v1/shipments/{id}/cancel" \
  -H "Authorization: Bearer dnx_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "Khách đổi ý"
}'
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (200)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000001001",
  "carrier": "ghn",
  "trackingCode": "GHNABC123",
  "status": "delivering",
  "orderId": "018f3b8e-1c2d-7a4b-9c3d-000000000101",
  "orderCode": "DH1024",
  "codAmount": "480000",
  "feeTotal": "30000",
  "createdAt": "2026-10-02T03:15:00.000Z",
  "expectedDeliveryAt": null,
  "failReason": null,
  "recipientName": "Nguyễn Văn An",
  "recipientPhone": "0901234567",
  "recipientAddress": "12 Lê Lợi, Thạch Thang, Hải Châu, Đà Nẵng",
  "weightGram": 500,
  "pickedUpAt": "2026-10-02T03:15:00.000Z",
  "events": []
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã vận đơn. |
| `carrier` | "ghn" \| "vtp" | có | Hãng vận chuyển. |
| `trackingCode` | string hoặc null | có | Mã vận đơn của hãng. Rỗng khi chưa đẩy sang hãng. |
| `status` | "pending" \| "delivering" \| "delivered" \| "delivery_failed" \| "returning" \| "returned" \| "partially_returned" \| "cancelled" \| "lost" \| "damaged" \| "exception" \| "scrapped" | có | Trạng thái chuẩn của vận đơn. |
| `orderId` | string (uuid) | có | Đơn hàng của vận đơn. |
| `orderCode` | string | có | Mã đơn hàng. |
| `codAmount` | string hoặc null | có | Tiền thu hộ (COD). Chuỗi thập phân hoặc `null`. |
| `feeTotal` | string hoặc null | có | Tổng phí vận chuyển. Chuỗi thập phân hoặc `null`. |
| `createdAt` | string (date-time) | có | Thời điểm tạo. ISO 8601, múi giờ UTC. |
| `expectedDeliveryAt` | string (date-time) hoặc null | có | Dự kiến giao. ISO 8601 UTC hoặc `null`. |
| `failReason` | string hoặc null | có | Lý do giao thất bại, nếu có. |
| `recipientName` | string hoặc null | có | Tên người nhận. |
| `recipientPhone` | string hoặc null | có | Số điện thoại người nhận. |
| `recipientAddress` | string hoặc null | có | Địa chỉ người nhận. |
| `weightGram` | integer hoặc null | có | Khối lượng khai với hãng, gram. |
| `pickedUpAt` | string (date-time) hoặc null | có | Thời điểm hãng lấy hàng. ISO 8601 UTC hoặc `null`. |
| `events` | array<object> | có | Hành trình, theo thứ tự thời gian. |
| `events[].id` | string (uuid) | có | Mã sự kiện. |
| `events[].status` | "pending" \| "delivering" \| "delivered" \| "delivery_failed" \| "returning" \| "returned" \| "partially_returned" \| "cancelled" \| "lost" \| "damaged" \| "exception" \| "scrapped" hoặc null | có | Trạng thái chuẩn sau sự kiện; `null` nếu sự kiện không đổi trạng thái. |
| `events[].carrierStatus` | string hoặc null | có | Mã trạng thái gốc của hãng. |
| `events[].occurredAt` | string (date-time) | có | Thời điểm hãng ghi nhận. ISO 8601, múi giờ UTC. |
| `events[].location` | string hoặc null | có | Nơi xảy ra. |
| `events[].description` | string hoặc null | có | Mô tả của hãng. |
| `events[].reason` | string hoặc null | có | Lý do (giao thất bại, hoàn…). |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 413 | [`payload-too-large`](/developers/errors#payload-too-large) | Thân yêu cầu vượt quá dung lượng cho phép |
| 415 | [`unsupported-media-type`](/developers/errors#unsupported-media-type) | Thân yêu cầu phải là JSON (`Content-Type: application/json`) |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |
| 409 | [`shipment-not-cancellable`](/developers/errors#shipment-not-cancellable) | Vận đơn không còn huỷ được |
| 503 | [`carrier-unavailable`](/developers/errors#carrier-unavailable) | Hãng vận chuyển hiện không với tới được, hãy thử lại sau |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

---

Nguồn: https://danix.vn/developers/reference/chat.md

# Tham chiếu: Hội thoại

Trang chat, hội thoại, tin nhắn, thẻ và bình luận. Mọi đường dẫn dưới `https://danix.vn/api/open/v1`. Đặc tả máy đọc: [openapi.json](/openapi.json).

## Các trang chat được dùng {#listPages}

`GET /pages`

Chỉ liệt kê những trang mà ứng dụng kết nối được chọn lúc tạo.

**Quyền cần có:** khoá có quyền `social.pages.read`.

**Ví dụ yêu cầu**

```bash
curl -X GET "https://danix.vn/api/open/v1/pages" \
  -H "Authorization: Bearer dnx_live_…"
```

**Phản hồi mẫu (200)**

```json
{
  "data": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000001301",
      "providerPageId": "1090000000000001",
      "provider": "facebook_page",
      "pageName": "Shop Mẫu",
      "avatarUrl": null,
      "status": "active"
    }
  ]
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `data` | array<object> | có | Các trang. |
| `data[].id` | string (uuid) | có | Mã trang trong DANIX. Đây là giá trị dùng làm `pageId` ở mọi chỗ khác của API. |
| `data[].providerPageId` | string | có | Mã trang ở kênh gốc (Facebook, Zalo…). Chỉ để đối chiếu, không dùng làm tham số. |
| `data[].provider` | "facebook_page" \| "zalo_oa" \| "zalo_personal" | có | Kênh: `facebook_page`, `zalo_oa` hoặc `zalo_personal`. |
| `data[].pageName` | string | có | Tên trang. |
| `data[].avatarUrl` | string hoặc null | có | Ảnh đại diện của trang, lưu trên kho của DANIX. `null` khi chưa có bản lưu. |
| `data[].status` | "active" \| "token_expired" \| "banned" | có | Tình trạng kết nối: `active`, `token_expired` hoặc `banned`. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Liệt kê hội thoại {#listConversations}

`GET /conversations`

Phân trang theo con trỏ, hội thoại có tin mới nhất trước. Lọc theo trang bằng `pageId` (mã trang trong DANIX, `id` của `GET /pages`); trang ngoài danh sách của ứng dụng bị từ chối `not-found`. Hội thoại chưa có tin nào không được liệt kê (vẫn đọc được theo id).

**Quyền cần có:** khoá có ít nhất một trong các quyền `social.conversations.read`, `social.conversations.reply` và quyền `social.pages.read`.

**Tham số query**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `limit` | integer | không | Số dòng mỗi trang, từ 1 đến 100. Mặc định 50. |
| `cursor` | string | không | Con trỏ lấy từ `nextCursor` của trang trước, để lật trang trong CÙNG một lượt đọc (giữ nguyên các tham số khác). Không tự dựng, không lưu để nối lượt đồng bộ sau. |
| `updatedSince` | string (date-time) | không | Chỉ lấy hội thoại có `lastMessageAt` từ thời điểm này (ISO 8601 UTC), tính cả mốc. `lastMessageAt` là giờ của tin mới nhất theo kênh (Facebook, Zalo), không phải mốc sửa: tin tới muộn hay lịch sử đồng bộ về sau có thể mang giờ trước mốc đã đọc, còn đổi thẻ, đánh dấu đã đọc hay đổi tên khách không làm hội thoại hiện lại. Không dùng để đồng bộ tăng dần. |
| `pageId` | string (uuid) | không | Chỉ lấy hội thoại của trang này: mã trang trong DANIX (`id` của `GET /pages`). |
| `unread` | "true" | không | `true` chỉ lấy hội thoại còn tin chưa đọc. |
| `tagId` | string (uuid) | không | Chỉ lấy hội thoại có thẻ này. |

**Ví dụ yêu cầu**

```bash
curl -X GET "https://danix.vn/api/open/v1/conversations" \
  -H "Authorization: Bearer dnx_live_…"
```

**Phản hồi mẫu (200)**

```json
{
  "data": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000001201",
      "pageId": "018f3b8e-1c2d-7a4b-9c3d-000000001301",
      "providerPageId": "1090000000000001",
      "provider": "facebook_page",
      "type": "INBOX",
      "customerName": "Nguyễn Văn An",
      "customerAvatarUrl": null,
      "unreadCount": 1,
      "lastMessageText": "Shop ơi áo này còn size M không?",
      "lastMessageAt": "2026-10-02T03:15:00.000Z",
      "lastMessageBy": "customer",
      "tags": [],
      "createdAt": "2026-10-01T03:15:00.000Z"
    }
  ],
  "nextCursor": null
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `data` | array<object> | có | Các bản ghi của trang này. |
| `data[].id` | string (uuid) | có | Mã hội thoại. |
| `data[].pageId` | string (uuid) | có | Mã trang trong DANIX (`id` của `GET /pages`). |
| `data[].providerPageId` | string | có | Mã trang ở kênh gốc. |
| `data[].provider` | "facebook_page" \| "zalo_oa" \| "zalo_personal" | có | Kênh của hội thoại. |
| `data[].type` | "INBOX" \| "COMMENT" \| "GROUP" | có | Loại: `INBOX` tin nhắn, `COMMENT` bình luận, `GROUP` nhóm. |
| `data[].customerName` | string hoặc null | có | Tên khách ở kênh gốc. |
| `data[].customerAvatarUrl` | string hoặc null | có | Ảnh đại diện của khách, lưu trên kho của DANIX. `null` khi chưa có bản lưu. |
| `data[].unreadCount` | integer | có | Số tin khách chưa được đọc. |
| `data[].lastMessageText` | string hoặc null | có | Nội dung tin gần nhất. |
| `data[].lastMessageAt` | string (date-time) hoặc null | có | Thời điểm tin gần nhất. ISO 8601 UTC hoặc `null`. |
| `data[].lastMessageBy` | "customer" \| "page" hoặc null | có | Ai gửi tin gần nhất. |
| `data[].tags` | array<object> | có | Các thẻ gắn vào hội thoại. |
| `data[].tags[].id` | string (uuid) | có | Mã thẻ. |
| `data[].tags[].name` | string | có | Tên thẻ. |
| `data[].createdAt` | string (date-time) | có | Thời điểm tạo hội thoại. ISO 8601, múi giờ UTC. |
| `nextCursor` | string hoặc null | có | Con trỏ của trang kế; `null` khi đã hết dữ liệu. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 400 | [`invalid-cursor`](/developers/errors#invalid-cursor) | Con trỏ phân trang không hợp lệ |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Chi tiết một hội thoại {#getConversation}

`GET /conversations/{id}`

Trả hội thoại kèm thẻ và số tin chưa đọc.

**Quyền cần có:** khoá có ít nhất một trong các quyền `social.conversations.read`, `social.conversations.reply` và quyền `social.pages.read`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Ví dụ yêu cầu**

```bash
curl -X GET "https://danix.vn/api/open/v1/conversations/{id}" \
  -H "Authorization: Bearer dnx_live_…"
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (200)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000001201",
  "pageId": "018f3b8e-1c2d-7a4b-9c3d-000000001301",
  "providerPageId": "1090000000000001",
  "provider": "facebook_page",
  "type": "INBOX",
  "customerName": "Nguyễn Văn An",
  "customerAvatarUrl": null,
  "unreadCount": 0,
  "lastMessageText": "Dạ còn ạ",
  "lastMessageAt": "2026-10-02T03:16:00.000Z",
  "lastMessageBy": "page",
  "tags": [],
  "createdAt": "2026-10-01T03:15:00.000Z"
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã hội thoại. |
| `pageId` | string (uuid) | có | Mã trang trong DANIX (`id` của `GET /pages`). |
| `providerPageId` | string | có | Mã trang ở kênh gốc. |
| `provider` | "facebook_page" \| "zalo_oa" \| "zalo_personal" | có | Kênh của hội thoại. |
| `type` | "INBOX" \| "COMMENT" \| "GROUP" | có | Loại: `INBOX` tin nhắn, `COMMENT` bình luận, `GROUP` nhóm. |
| `customerName` | string hoặc null | có | Tên khách ở kênh gốc. |
| `customerAvatarUrl` | string hoặc null | có | Ảnh đại diện của khách, lưu trên kho của DANIX. `null` khi chưa có bản lưu. |
| `unreadCount` | integer | có | Số tin khách chưa được đọc. |
| `lastMessageText` | string hoặc null | có | Nội dung tin gần nhất. |
| `lastMessageAt` | string (date-time) hoặc null | có | Thời điểm tin gần nhất. ISO 8601 UTC hoặc `null`. |
| `lastMessageBy` | "customer" \| "page" hoặc null | có | Ai gửi tin gần nhất. |
| `tags` | array<object> | có | Các thẻ gắn vào hội thoại. |
| `tags[].id` | string (uuid) | có | Mã thẻ. |
| `tags[].name` | string | có | Tên thẻ. |
| `createdAt` | string (date-time) | có | Thời điểm tạo hội thoại. ISO 8601, múi giờ UTC. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Đánh dấu hội thoại đã đọc {#markConversationRead}

`POST /conversations/{id}/read`

Đặt số tin chưa đọc về 0.

**Quyền cần có:** khoá có ít nhất một trong các quyền `social.conversations.read`, `social.conversations.reply` và quyền `social.pages.read`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Ví dụ yêu cầu**

```bash
curl -X POST "https://danix.vn/api/open/v1/conversations/{id}/read" \
  -H "Authorization: Bearer dnx_live_…"
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (200)**

```json
{
  "ok": true
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `ok` | true | có | Luôn `true` khi thao tác thành công. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Tin nhắn của hội thoại {#listMessages}

`GET /conversations/{id}/messages`

Phân trang theo con trỏ, tin mới nhất trước.

**Quyền cần có:** khoá có ít nhất một trong các quyền `social.conversations.read`, `social.conversations.reply` và quyền `social.pages.read`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Tham số query**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `cursor` | string | không | Con trỏ lấy từ `nextCursor` của trang trước. |
| `limit` | integer | không | Số tin mỗi trang, 1 đến 100. |

**Ví dụ yêu cầu**

```bash
curl -X GET "https://danix.vn/api/open/v1/conversations/{id}/messages" \
  -H "Authorization: Bearer dnx_live_…"
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (200)**

```json
{
  "data": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000001101",
      "conversationId": "018f3b8e-1c2d-7a4b-9c3d-000000001201",
      "pageId": "018f3b8e-1c2d-7a4b-9c3d-000000001301",
      "providerPageId": "1090000000000001",
      "provider": "facebook_page",
      "direction": "outbound",
      "type": "text",
      "text": "Dạ còn ạ, anh chị cần mấy cái ạ?",
      "status": "sent",
      "isDeleted": false,
      "sentByName": "Trần Thị Bình",
      "sentByIsIntegration": false,
      "customerName": "Nguyễn Văn An",
      "attachments": [],
      "createdAt": "2026-10-02T03:15:00.000Z"
    }
  ],
  "nextCursor": null
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `data` | array<object> | có | Các bản ghi của trang này. |
| `data[].id` | string (uuid) | có | Mã tin nhắn. |
| `data[].conversationId` | string (uuid) | có | Hội thoại chứa tin. |
| `data[].pageId` | string (uuid) | có | Mã trang trong DANIX (`id` của `GET /pages`). |
| `data[].providerPageId` | string | có | Mã trang ở kênh gốc. |
| `data[].provider` | "facebook_page" \| "zalo_oa" \| "zalo_personal" | có | Kênh của tin nhắn. |
| `data[].direction` | "inbound" \| "outbound" | có | `inbound` khách gửi, `outbound` shop gửi. |
| `data[].type` | "text" \| "image" \| "video" \| "audio" \| "file" \| "sticker" \| "location" \| "reel" \| "share" \| "like" \| "postback" \| "order" \| "referral" | có | Loại tin nhắn. |
| `data[].text` | string hoặc null | có | Nội dung chữ. |
| `data[].status` | "pending" \| "sent" \| "delivered" \| "read" \| "failed" | có | Trạng thái gửi. |
| `data[].isDeleted` | boolean | có | Tin đã bị thu hồi. |
| `data[].sentByName` | string hoặc null | có | Tên nhân viên hay ứng dụng đã gửi; rỗng với tin của khách. |
| `data[].sentByIsIntegration` | boolean | có | `true` khi tin do một ứng dụng kết nối gửi. |
| `data[].customerName` | string hoặc null | có | Tên khách của hội thoại; với nhóm (`GROUP`) là người gửi tin. |
| `data[].attachments` | array<object> | có | Tệp đính kèm. |
| `data[].attachments[].id` | string (uuid) | có | Mã tệp đính kèm. |
| `data[].attachments[].type` | "image" \| "video" \| "audio" \| "file" \| "link" \| "button" | có | Loại: bốn loại đầu là tệp; `link` và `button` là nút của tin mẫu. |
| `data[].attachments[].url` | string hoặc null | có | Tệp: bản GỐC trên kho của DANIX, `null` khi chưa lưu về (xem `originalState`). Loại `link`: địa chỉ nút trỏ tới. Không bao giờ là đường dẫn của Facebook hay Zalo. |
| `data[].attachments[].previewUrl` | string hoặc null | có | Bản xem trước (ảnh thu nhỏ, ảnh bìa video) trên kho của DANIX; `null` khi không có. |
| `data[].attachments[].originalState` | "pending" \| "stored" \| "gone" | có | `stored`: bản gốc có ở `url`. `pending`: chưa có bản gốc trên kho của DANIX. Tệp của hội thoại lâu không hoạt động đã được cất đi; lượt đọc trang ĐẦU tin nhắn (`GET /conversations/{id}/messages` không `cursor`) hay lượt nhân viên mở hội thoại kéo nó về ở nền trong ít phút — đọc lại sau để nhận `url`. Tệp chưa từng lưu bản gốc thì được lưu khi nhân viên mở xem. `gone`: nguồn không còn, sẽ không bao giờ có. |
| `data[].attachments[].fileName` | string hoặc null | có | Tên tệp. |
| `data[].attachments[].mimeType` | string hoặc null | có | Loại nội dung (MIME). |
| `data[].createdAt` | string (date-time) | có | Thời điểm tin được ghi nhận. ISO 8601, múi giờ UTC. |
| `nextCursor` | string hoặc null | có | Con trỏ của trang kế; `null` khi đã hết dữ liệu. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |
| 400 | [`invalid-cursor`](/developers/errors#invalid-cursor) | Con trỏ phân trang không hợp lệ |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Gửi tin nhắn {#sendMessage}

`POST /conversations/{id}/messages`

Gửi một tin nhắn chữ trong hội thoại. Gửi kèm `Idempotency-Key`: cùng khoá trong CÙNG hội thoại không gửi hai lần (cùng khoá ở hội thoại khác là một tin mới). Ngoài khung thời gian cho phép của kênh trả `messaging-window-closed`.

**Quyền cần có:** khoá có quyền `social.conversations.reply` và quyền `social.pages.read`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Header**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | không | Khoá chống trùng do client đặt (khuyến nghị UUID). Gửi lại cùng khoá trong 24 giờ trả đúng kết quả lần đầu và header `Idempotent-Replayed: true`. |

**Thân yêu cầu (JSON)**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `text` | string | có | Nội dung tin nhắn. |
| `replyToMessageId` | string (uuid) | không | Trả lời một tin có sẵn của hội thoại. |

**Ví dụ yêu cầu**

```bash
curl -X POST "https://danix.vn/api/open/v1/conversations/{id}/messages" \
  -H "Authorization: Bearer dnx_live_…" \
  -H "Idempotency-Key: a58738ee-0d77-444d-96c4-c547752c5c17" \
  -H "Content-Type: application/json" \
  -d '{
  "text": "Dạ còn ạ, anh chị cần mấy cái ạ?"
}'
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (201)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000001101",
  "conversationId": "018f3b8e-1c2d-7a4b-9c3d-000000001201",
  "pageId": "018f3b8e-1c2d-7a4b-9c3d-000000001301",
  "providerPageId": "1090000000000001",
  "provider": "facebook_page",
  "direction": "outbound",
  "type": "text",
  "text": "Dạ còn ạ, anh chị cần mấy cái ạ?",
  "status": "sent",
  "isDeleted": false,
  "sentByName": "Trần Thị Bình",
  "sentByIsIntegration": false,
  "customerName": "Nguyễn Văn An",
  "attachments": [],
  "createdAt": "2026-10-02T03:15:00.000Z"
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã tin nhắn. |
| `conversationId` | string (uuid) | có | Hội thoại chứa tin. |
| `pageId` | string (uuid) | có | Mã trang trong DANIX (`id` của `GET /pages`). |
| `providerPageId` | string | có | Mã trang ở kênh gốc. |
| `provider` | "facebook_page" \| "zalo_oa" \| "zalo_personal" | có | Kênh của tin nhắn. |
| `direction` | "inbound" \| "outbound" | có | `inbound` khách gửi, `outbound` shop gửi. |
| `type` | "text" \| "image" \| "video" \| "audio" \| "file" \| "sticker" \| "location" \| "reel" \| "share" \| "like" \| "postback" \| "order" \| "referral" | có | Loại tin nhắn. |
| `text` | string hoặc null | có | Nội dung chữ. |
| `status` | "pending" \| "sent" \| "delivered" \| "read" \| "failed" | có | Trạng thái gửi. |
| `isDeleted` | boolean | có | Tin đã bị thu hồi. |
| `sentByName` | string hoặc null | có | Tên nhân viên hay ứng dụng đã gửi; rỗng với tin của khách. |
| `sentByIsIntegration` | boolean | có | `true` khi tin do một ứng dụng kết nối gửi. |
| `customerName` | string hoặc null | có | Tên khách của hội thoại; với nhóm (`GROUP`) là người gửi tin. |
| `attachments` | array<object> | có | Tệp đính kèm. |
| `attachments[].id` | string (uuid) | có | Mã tệp đính kèm. |
| `attachments[].type` | "image" \| "video" \| "audio" \| "file" \| "link" \| "button" | có | Loại: bốn loại đầu là tệp; `link` và `button` là nút của tin mẫu. |
| `attachments[].url` | string hoặc null | có | Tệp: bản GỐC trên kho của DANIX, `null` khi chưa lưu về (xem `originalState`). Loại `link`: địa chỉ nút trỏ tới. Không bao giờ là đường dẫn của Facebook hay Zalo. |
| `attachments[].previewUrl` | string hoặc null | có | Bản xem trước (ảnh thu nhỏ, ảnh bìa video) trên kho của DANIX; `null` khi không có. |
| `attachments[].originalState` | "pending" \| "stored" \| "gone" | có | `stored`: bản gốc có ở `url`. `pending`: chưa có bản gốc trên kho của DANIX. Tệp của hội thoại lâu không hoạt động đã được cất đi; lượt đọc trang ĐẦU tin nhắn (`GET /conversations/{id}/messages` không `cursor`) hay lượt nhân viên mở hội thoại kéo nó về ở nền trong ít phút — đọc lại sau để nhận `url`. Tệp chưa từng lưu bản gốc thì được lưu khi nhân viên mở xem. `gone`: nguồn không còn, sẽ không bao giờ có. |
| `attachments[].fileName` | string hoặc null | có | Tên tệp. |
| `attachments[].mimeType` | string hoặc null | có | Loại nội dung (MIME). |
| `createdAt` | string (date-time) | có | Thời điểm tin được ghi nhận. ISO 8601, múi giờ UTC. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 413 | [`payload-too-large`](/developers/errors#payload-too-large) | Thân yêu cầu vượt quá dung lượng cho phép |
| 415 | [`unsupported-media-type`](/developers/errors#unsupported-media-type) | Thân yêu cầu phải là JSON (`Content-Type: application/json`) |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |
| 409 | [`idempotency-key-in-progress`](/developers/errors#idempotency-key-in-progress) | Yêu cầu với cùng Idempotency-Key đang được xử lý |
| 422 | [`idempotency-key-reused`](/developers/errors#idempotency-key-reused) | Idempotency-Key đã dùng cho một yêu cầu có nội dung khác |
| 422 | [`messaging-window-closed`](/developers/errors#messaging-window-closed) | Đã quá khung thời gian được phép nhắn cho khách này |
| 422 | [`page-not-connected`](/developers/errors#page-not-connected) | Trang chat không còn kết nối |
| 422 | [`conversation-not-replyable`](/developers/errors#conversation-not-replyable) | Hội thoại này không trả lời được |
| 503 | [`channel-unavailable`](/developers/errors#channel-unavailable) | Kênh chat tạm thời không phản hồi, hãy thử lại sau |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Các thẻ hội thoại {#listChatTags}

`GET /tags`

Thẻ hội thoại của các trang ứng dụng được dùng. Thẻ là của từng trang (`pageId`): chỉ gắn được vào hội thoại cùng trang.

**Quyền cần có:** khoá có ít nhất một trong các quyền `social.conversations.read`, `social.conversations.reply` và quyền `social.pages.read`.

**Ví dụ yêu cầu**

```bash
curl -X GET "https://danix.vn/api/open/v1/tags" \
  -H "Authorization: Bearer dnx_live_…"
```

**Phản hồi mẫu (200)**

```json
{
  "data": [
    {
      "id": "018f3b8e-1c2d-7a4b-9c3d-000000001401",
      "name": "Khách quen",
      "color": "blue",
      "pageId": "018f3b8e-1c2d-7a4b-9c3d-000000001301"
    }
  ]
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `data` | array<object> | có | Các thẻ. |
| `data[].id` | string (uuid) | có | Mã thẻ. |
| `data[].name` | string | có | Tên thẻ. |
| `data[].color` | "red" \| "orange" \| "amber" \| "green" \| "teal" \| "cyan" \| "blue" \| "indigo" \| "purple" \| "pink" | có | Màu của thẻ. |
| `data[].pageId` | string (uuid) | có | Trang sở hữu thẻ. Thẻ là của TỪNG trang: chỉ gắn được vào hội thoại cùng trang, và hai trang có thể có thẻ trùng tên. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Gắn thẻ vào hội thoại {#addConversationTag}

`POST /conversations/{id}/tags`

Gắn một thẻ có sẵn vào hội thoại. Gắn lại thẻ đã có là thành công.

**Quyền cần có:** khoá có quyền `social.conversations.reply` và quyền `social.pages.read`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Thân yêu cầu (JSON)**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `tagId` | string (uuid) | có | Mã thẻ cần gắn. |

**Ví dụ yêu cầu**

```bash
curl -X POST "https://danix.vn/api/open/v1/conversations/{id}/tags" \
  -H "Authorization: Bearer dnx_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "tagId": "018f3b8e-1c2d-7a4b-9c3d-000000001401"
}'
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (200)**

```json
{
  "ok": true
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `ok` | true | có | Luôn `true` khi thao tác thành công. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 413 | [`payload-too-large`](/developers/errors#payload-too-large) | Thân yêu cầu vượt quá dung lượng cho phép |
| 415 | [`unsupported-media-type`](/developers/errors#unsupported-media-type) | Thân yêu cầu phải là JSON (`Content-Type: application/json`) |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Gỡ thẻ khỏi hội thoại {#removeConversationTag}

`DELETE /conversations/{id}/tags/{tagId}`

Gỡ một thẻ khỏi hội thoại.

**Quyền cần có:** khoá có quyền `social.conversations.reply` và quyền `social.pages.read`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |
| `tagId` | string (uuid) | có | Mã thẻ. |

**Ví dụ yêu cầu**

```bash
curl -X DELETE "https://danix.vn/api/open/v1/conversations/{id}/tags/{tagId}" \
  -H "Authorization: Bearer dnx_live_…"
```

Thay `{id}`, `{tagId}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (200)**

```json
{
  "ok": true
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `ok` | true | có | Luôn `true` khi thao tác thành công. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Ẩn hoặc hiện một bình luận {#setCommentHidden}

`POST /comments/{id}/hidden`

Chỉ áp dụng cho bình luận (`type = COMMENT`) của trang Facebook.

**Quyền cần có:** khoá có quyền `social.conversations.reply` và quyền `social.pages.read`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Thân yêu cầu (JSON)**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `hidden` | boolean | có | `true` ẩn bình luận, `false` hiện lại. |

**Ví dụ yêu cầu**

```bash
curl -X POST "https://danix.vn/api/open/v1/comments/{id}/hidden" \
  -H "Authorization: Bearer dnx_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "hidden": true
}'
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (200)**

```json
{
  "ok": true
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `ok` | true | có | Luôn `true` khi thao tác thành công. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 413 | [`payload-too-large`](/developers/errors#payload-too-large) | Thân yêu cầu vượt quá dung lượng cho phép |
| 415 | [`unsupported-media-type`](/developers/errors#unsupported-media-type) | Thân yêu cầu phải là JSON (`Content-Type: application/json`) |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |
| 422 | [`page-not-connected`](/developers/errors#page-not-connected) | Trang chat không còn kết nối |
| 422 | [`conversation-not-replyable`](/developers/errors#conversation-not-replyable) | Hội thoại này không trả lời được |
| 503 | [`channel-unavailable`](/developers/errors#channel-unavailable) | Kênh chat tạm thời không phản hồi, hãy thử lại sau |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).

## Nhắn riêng cho người bình luận {#sendPrivateReply}

`POST /comments/{id}/private-reply`

Gửi một tin nhắn riêng đáp lại bình luận. Mỗi bình luận chỉ nhắn riêng được một lần.

**Quyền cần có:** khoá có quyền `social.conversations.reply` và quyền `social.pages.read`.

**Tham số đường dẫn**

| Tên | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã định danh (UUID) của tài nguyên. |

**Thân yêu cầu (JSON)**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `text` | string | có | Nội dung tin nhắn riêng gửi cho người bình luận. |

**Ví dụ yêu cầu**

```bash
curl -X POST "https://danix.vn/api/open/v1/comments/{id}/private-reply" \
  -H "Authorization: Bearer dnx_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "text": "Cảm ơn bạn đã quan tâm, shop nhắn riêng nhé"
}'
```

Thay `{id}` trong địa chỉ bằng mã thật của bản ghi, và `dnx_live_…` bằng khoá của bạn.

**Phản hồi mẫu (201)**

```json
{
  "id": "018f3b8e-1c2d-7a4b-9c3d-000000001101",
  "conversationId": "018f3b8e-1c2d-7a4b-9c3d-000000001201",
  "pageId": "018f3b8e-1c2d-7a4b-9c3d-000000001301",
  "providerPageId": "1090000000000001",
  "provider": "facebook_page",
  "direction": "outbound",
  "type": "text",
  "text": "Dạ còn ạ, anh chị cần mấy cái ạ?",
  "status": "sent",
  "isDeleted": false,
  "sentByName": "Trần Thị Bình",
  "sentByIsIntegration": false,
  "customerName": "Nguyễn Văn An",
  "attachments": [],
  "createdAt": "2026-10-02T03:15:00.000Z"
}
```

**Trường của phản hồi**

| Trường | Kiểu | Bắt buộc | Mô tả |
| --- | --- | --- | --- |
| `id` | string (uuid) | có | Mã tin nhắn. |
| `conversationId` | string (uuid) | có | Hội thoại chứa tin. |
| `pageId` | string (uuid) | có | Mã trang trong DANIX (`id` của `GET /pages`). |
| `providerPageId` | string | có | Mã trang ở kênh gốc. |
| `provider` | "facebook_page" \| "zalo_oa" \| "zalo_personal" | có | Kênh của tin nhắn. |
| `direction` | "inbound" \| "outbound" | có | `inbound` khách gửi, `outbound` shop gửi. |
| `type` | "text" \| "image" \| "video" \| "audio" \| "file" \| "sticker" \| "location" \| "reel" \| "share" \| "like" \| "postback" \| "order" \| "referral" | có | Loại tin nhắn. |
| `text` | string hoặc null | có | Nội dung chữ. |
| `status` | "pending" \| "sent" \| "delivered" \| "read" \| "failed" | có | Trạng thái gửi. |
| `isDeleted` | boolean | có | Tin đã bị thu hồi. |
| `sentByName` | string hoặc null | có | Tên nhân viên hay ứng dụng đã gửi; rỗng với tin của khách. |
| `sentByIsIntegration` | boolean | có | `true` khi tin do một ứng dụng kết nối gửi. |
| `customerName` | string hoặc null | có | Tên khách của hội thoại; với nhóm (`GROUP`) là người gửi tin. |
| `attachments` | array<object> | có | Tệp đính kèm. |
| `attachments[].id` | string (uuid) | có | Mã tệp đính kèm. |
| `attachments[].type` | "image" \| "video" \| "audio" \| "file" \| "link" \| "button" | có | Loại: bốn loại đầu là tệp; `link` và `button` là nút của tin mẫu. |
| `attachments[].url` | string hoặc null | có | Tệp: bản GỐC trên kho của DANIX, `null` khi chưa lưu về (xem `originalState`). Loại `link`: địa chỉ nút trỏ tới. Không bao giờ là đường dẫn của Facebook hay Zalo. |
| `attachments[].previewUrl` | string hoặc null | có | Bản xem trước (ảnh thu nhỏ, ảnh bìa video) trên kho của DANIX; `null` khi không có. |
| `attachments[].originalState` | "pending" \| "stored" \| "gone" | có | `stored`: bản gốc có ở `url`. `pending`: chưa có bản gốc trên kho của DANIX. Tệp của hội thoại lâu không hoạt động đã được cất đi; lượt đọc trang ĐẦU tin nhắn (`GET /conversations/{id}/messages` không `cursor`) hay lượt nhân viên mở hội thoại kéo nó về ở nền trong ít phút — đọc lại sau để nhận `url`. Tệp chưa từng lưu bản gốc thì được lưu khi nhân viên mở xem. `gone`: nguồn không còn, sẽ không bao giờ có. |
| `attachments[].fileName` | string hoặc null | có | Tên tệp. |
| `attachments[].mimeType` | string hoặc null | có | Loại nội dung (MIME). |
| `createdAt` | string (date-time) | có | Thời điểm tin được ghi nhận. ISO 8601, múi giờ UTC. |

**Lỗi**

| Trạng thái | Mã `code` | Ý nghĩa |
| --- | --- | --- |
| 401 | [`invalid-api-key`](/developers/errors#invalid-api-key) | Khoá API thiếu, sai hoặc đã bị thu hồi |
| 403 | [`insufficient-permission`](/developers/errors#insufficient-permission) | Khoá API không có quyền thực hiện thao tác này |
| 403 | [`shop-suspended`](/developers/errors#shop-suspended) | Shop đang bị đình chỉ |
| 403 | [`automation-not-active`](/developers/errors#automation-not-active) | Shop chưa bật tính năng Tự động hoá |
| 429 | [`rate-limited`](/developers/errors#rate-limited) | Vượt hạn mức gọi API, hãy chờ rồi thử lại |
| 500 | [`internal-error`](/developers/errors#internal-error) | Lỗi hệ thống |
| 402 | [`subscription-expired`](/developers/errors#subscription-expired) | Gói cước của shop đã hết hạn, chỉ còn quyền đọc |
| 400 | [`validation-failed`](/developers/errors#validation-failed) | Dữ liệu gửi lên không hợp lệ |
| 413 | [`payload-too-large`](/developers/errors#payload-too-large) | Thân yêu cầu vượt quá dung lượng cho phép |
| 415 | [`unsupported-media-type`](/developers/errors#unsupported-media-type) | Thân yêu cầu phải là JSON (`Content-Type: application/json`) |
| 404 | [`not-found`](/developers/errors#not-found) | Không tìm thấy tài nguyên |
| 409 | [`conflict`](/developers/errors#conflict) | Thao tác xung đột với trạng thái hiện tại |
| 422 | [`messaging-window-closed`](/developers/errors#messaging-window-closed) | Đã quá khung thời gian được phép nhắn cho khách này |
| 422 | [`page-not-connected`](/developers/errors#page-not-connected) | Trang chat không còn kết nối |
| 422 | [`conversation-not-replyable`](/developers/errors#conversation-not-replyable) | Hội thoại này không trả lời được |
| 503 | [`channel-unavailable`](/developers/errors#channel-unavailable) | Kênh chat tạm thời không phản hồi, hãy thử lại sau |

Định dạng lỗi và bảng mọi mã `code`: [Lỗi](/developers/errors).
