# REST API Hỗ trợ

REST API của Hỗ trợ cho server của bạn làm việc với hộp thư như một nhân viên: liệt kê hội thoại từ mọi kênh, đọc dòng thời gian, trả lời hoặc ghi chú nội bộ, giao việc, gắn tag và đánh dấu đã xử lý. Dùng nó để nối hộp thư với ERP, CRM, hệ thống đơn hàng hay bot của riêng bạn. Webhook báo cho bạn khi có chuyện xảy ra, nên không cần gọi API liên tục để dò.

## Trước khi bắt đầu

- Một API key tạo trong dashboard (**API Keys**). Gửi trong header `Authorization: Bearer ht_...` tới `https://api.hatang.dev`. Xem [Xác thực](https://hatang.dev/vi/docs/guides/getting-started/authentication.md).
- Key cần đúng scope:

  | Scope | Cho phép |
  |---|---|
  | `chat:read` | Liệt kê, đọc site, hội thoại, dòng thời gian, khách truy cập, phiếu hỗ trợ, báo cáo, kịch bản, chu trình, chiến dịch, câu trả lời mẫu, quy tắc phân phối và nguồn kiến thức |
  | `chat:write` | Mọi thứ của `chat:read`, cộng với trả lời, cập nhật hội thoại, phiếu, site, kịch bản, chu trình, chiến dịch, câu trả lời mẫu, quy tắc phân phối và nguồn kiến thức |
  | `crm:read` / `crm:write` | Khách hàng, trường tuỳ chỉnh của khách hàng và Zalo ZNS (key có scope `chat:*` cũng dùng được) |
  | `products:read` / `products:write` | Sản phẩm và đơn hàng (key có scope `chat:*` cũng dùng được) |

  Scope `write` bao gồm `read`; `chat:*` và `*` bao trùm tất cả.
- Module Hỗ trợ phải đang bật cho doanh nghiệp. Khi module tắt, mọi endpoint trả `403 product_disabled` ([Bật/tắt module](https://hatang.dev/vi/help/getting-started/modules)).

> **Note**
>
> `chat:reply` và `chat:manage` là quyền của nhân viên trong dashboard ([Nhân viên và phân quyền](https://hatang.dev/vi/help/account/employees-and-roles)), không phải scope của API key.

## 1. Liệt kê hội thoại

`GET /chat/conversations` trả về hội thoại có hoạt động mới nhất trước, mặc định 50 hội thoại mỗi trang (tối đa 100).

| Tham số query | Ý nghĩa |
|---|---|
| `status` | Phân tách bằng dấu phẩy: `open`, `pending`, `resolved`, `closed` |
| `unanswered` | `true`: chỉ hội thoại mà khách là người nhắn cuối |
| `assignee_id` | Hội thoại đang giao cho nhân viên này |
| `site_id` | Hội thoại của một site |
| `visitor_id` | Hội thoại của một khách truy cập |
| `q` | Từ khoá tìm kiếm (tối đa 200 ký tự) |
| `flag` | `hidden` (đang tạm ẩn), `spam` hoặc `none`. Mặc định chỉ trả hội thoại không gắn cờ |
| `limit` | 1–100, mặc định 50 |
| `after` | Con trỏ: `id` của hội thoại cuối cùng ở trang trước |

**cURL**

```bash
curl "https://api.hatang.dev/chat/conversations?status=open,pending&unanswered=true&limit=20" \
  -H "Authorization: Bearer ht_123..."
```

**Node.js**

```js
import { Hatang } from "@hatang/sdk";

const hatang = new Hatang("ht_123...");

const { data, error } = await hatang.chat.conversations.list({
  status: ["open", "pending"],
  unanswered: true,
  limit: 20,
});
if (error) throw new Error(error.message);
console.log(data.data.map((c) => `${c.id} ${c.channel} ${c.last_message_preview}`));
```

**Python**

```python
from hatang import Hatang

hatang = Hatang("ht_123...")
page = hatang.chat.conversations.list(status=["open", "pending"], unanswered=True, limit=20)
for c in page["data"]:
    print(c["id"], c["channel"], c["last_message_preview"])
```

**PHP**

```php
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.hatang.dev']);
$res = $client->get('/chat/conversations', [
    'headers' => ['Authorization' => 'Bearer ht_123...'],
    'query'   => ['status' => 'open,pending', 'unanswered' => 'true', 'limit' => 20],
]);
$page = json_decode((string) $res->getBody(), true);
```

```json title="Response (rút gọn)"
{
  "object": "list",
  "has_more": true,
  "data": [
    {
      "id": "cnv_...",
      "site_id": "site_...",
      "visitor_id": "vis_...",
      "channel": "zalo",
      "channel_account_id": "...",
      "status": "open",
      "assignee_kind": null,
      "assignee_id": null,
      "seq": 14,
      "agent_unread": 2,
      "visitor_unread": 0,
      "last_message_at": "2026-10-23T08:15:02.000Z",
      "last_message_preview": "Shop còn size M không ạ?",
      "last_message_sender_type": "visitor",
      "first_response_at": null,
      "tags": [],
      "member_ids": [],
      "flag": null,
      "subject": null,
      "created_at": "2026-10-23T08:14:40.000Z",
      "updated_at": "2026-10-23T08:15:02.000Z",
      "visitor": { "id": "vis_...", "name": "Lan", "...": "..." }
    }
  ]
}
```

`channel` là một trong `web`, `zalo`, `zalo_personal`, `messenger`, `instagram`, `tiktok`, `tiktokshop`, `whatsapp`, `telegram`, `x`, `line` hoặc `email`. Hội thoại có cùng các trường này ở mọi nơi, kể cả trong webhook.

> **Caution**
>
> Tham số boolean trên query được hiểu là "có mặt nghĩa là true". Gửi `unanswered=true` để lọc, còn không thì bỏ hẳn tham số; `unanswered=false` không tắt bộ lọc.

## 2. Đọc dòng thời gian

`GET /chat/conversations/{id}/messages` trả về tin nhắn và sự kiện theo thứ tự `seq`. Mỗi phần tử có `kind: "message"` hoặc `kind: "event"`.

| Tham số query | Ý nghĩa |
|---|---|
| `after_seq` | Trả các phần tử sau `seq` này (cũ trước). Không truyền thì nhận trang mới nhất |
| `limit` | 1–200, mặc định 100 |
| `include_private` | `true` để lấy cả ghi chú nội bộ. Bỏ trống để không lấy |

**cURL**

```bash
curl "https://api.hatang.dev/chat/conversations/cnv_123/messages?after_seq=10" \
  -H "Authorization: Bearer ht_123..."
```

**Node.js**

```js
const { data } = await hatang.chat.conversations.messages("cnv_123", { after_seq: 10 });
for (const item of data.data) {
  if (item.kind === "message") console.log(item.seq, item.sender_type, item.content);
}
```

**Python**

```python
items = hatang.chat.conversations.messages("cnv_123", after_seq=10)["data"]
```

**PHP**

```php
$res = $client->get('/chat/conversations/cnv_123/messages', [
    'headers' => ['Authorization' => 'Bearer ht_123...'],
    'query'   => ['after_seq' => 10],
]);
```

```json title="Một tin nhắn"
{
  "kind": "message",
  "id": "msg_...",
  "conversation_id": "cnv_123",
  "seq": 11,
  "sender_type": "visitor",
  "sender_id": "vis_...",
  "sender_name": "Lan",
  "visibility": "public",
  "content_type": "text",
  "content": "Shop còn size M không ạ?",
  "attachments": [],
  "metadata": {},
  "delivery_status": null,
  "delivery_error": null,
  "read_at": null,
  "created_at": "2026-10-23T08:15:02.000Z"
}
```

`sender_type` là `visitor`, `agent`, `bot` hoặc `system`. Tệp đính kèm có `url` ký sẵn, dùng được trong 24 giờ; hết hạn thì đọc lại tin để có link mới.

## 3. Trả lời hoặc ghi chú

`POST /chat/conversations/{id}/messages` gửi một tin dưới tên đội của bạn. Hạ Tầng chuyển tin qua đúng kênh của hội thoại: widget, Zalo, Messenger, email…

| Trường | Kiểu | Ghi chú |
|---|---|---|
| `content` | string | Bắt buộc, 1–10.000 ký tự |
| `visibility` | `public` \| `private` | Mặc định `public`. `private` là ghi chú nội bộ, khách không thấy |
| `sender_name` | string | Tên khách nhìn thấy, tối đa 100 ký tự. Mặc định `Support` |
| `attachments` | string[] | Tối đa 5 id của tệp đã tải lên hội thoại |
| `metadata` | object | Thẻ (`product`, `order`, `payment`, `quick_replies`), tối đa 4 KB |

Gửi kèm header `Idempotency-Key` để thử lại an toàn. Request thứ hai cùng key trong cùng hội thoại trả về tin gốc với mã `200` và `"duplicate": true` thay vì gửi hai lần.

**cURL**

```bash
curl -X POST "https://api.hatang.dev/chat/conversations/cnv_123/messages" \
  -H "Authorization: Bearer ht_123..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-8812-shipped" \
  -d '{"content": "Đơn #8812 đã giao cho đơn vị vận chuyển, dự kiến tới vào thứ Sáu.", "sender_name": "Shop Trà Xanh"}'
```

**Node.js**

```js
const { data, error } = await hatang.chat.conversations.send(
  "cnv_123",
  { content: "Đơn #8812 đã giao cho đơn vị vận chuyển, dự kiến tới vào thứ Sáu.", sender_name: "Shop Trà Xanh" },
  { idempotencyKey: "order-8812-shipped" },
);
```

**Python**

```python
hatang.chat.conversations.send(
    "cnv_123",
    {"content": "Đơn #8812 đã giao cho đơn vị vận chuyển, dự kiến tới vào thứ Sáu.", "sender_name": "Shop Trà Xanh"},
    idempotency_key="order-8812-shipped",
)
```

**PHP**

```php
$res = $client->post('/chat/conversations/cnv_123/messages', [
    'headers' => ['Authorization' => 'Bearer ht_123...', 'Idempotency-Key' => 'order-8812-shipped'],
    'json'    => ['content' => 'Đơn #8812 đã giao cho đơn vị vận chuyển, dự kiến tới vào thứ Sáu.', 'sender_name' => 'Shop Trà Xanh'],
]);
```

```json title="201 Created"
{ "object": "chat_message", "id": "msg_...", "seq": 15, "conversation_id": "cnv_123", "duplicate": false }
```

Lỗi có thể gặp:

- `409 conversation_closed`: hội thoại đã `closed`, không nhận thêm tin.
- `422 validation_error`: `content` trống, quá dài, hoặc `metadata` vượt 4 KB.
- Trên kênh mạng xã hội, việc gửi diễn ra bất đồng bộ. API nhận tin ngay; nếu sau đó nền tảng từ chối (ví dụ đã quá cửa sổ 24 giờ của Messenger hay WhatsApp), tin có `delivery_status: "failed"` kèm `delivery_error`. Đọc lại dòng thời gian hoặc xem trong hộp thư để biết. Trang của từng kênh giải thích quy định riêng của nền tảng đó.

Để gửi thẻ sản phẩm, dùng `POST /chat/conversations/{id}/products` với `product_ids` (tối đa 10) và `intro` tuỳ chọn.

## 4. Giao việc, gắn tag, tạm ẩn và xử lý xong

`PATCH /chat/conversations/{id}` đổi một hoặc nhiều trường:

| Trường | Giá trị |
|---|---|
| `status` | `open`, `pending`, `resolved`, `closed` |
| `assignee_id` | User id của nhân viên, hoặc `null` để bỏ giao |
| `tags` | Toàn bộ danh sách tag (thay danh sách hiện tại; tối đa 20 tag, mỗi tag tối đa 40 ký tự) |
| `member_ids` | Toàn bộ danh sách nhân viên cùng theo dõi hội thoại (tối đa 20) |
| `flag` | `hidden` (tạm ẩn), `spam`, hoặc `null` để bỏ cờ |

**cURL**

```bash
curl -X PATCH "https://api.hatang.dev/chat/conversations/cnv_123" \
  -H "Authorization: Bearer ht_123..." \
  -H "Content-Type: application/json" \
  -d '{"status": "resolved", "tags": ["shipping"]}'
```

**Node.js**

```js
await hatang.chat.conversations.update("cnv_123", { status: "resolved", tags: ["shipping"] });
```

**Python**

```python
hatang.chat.conversations.update("cnv_123", {"status": "resolved", "tags": ["shipping"]})
```

**PHP**

```php
$client->patch('/chat/conversations/cnv_123', [
    'headers' => ['Authorization' => 'Bearer ht_123...'],
    'json'    => ['status' => 'resolved', 'tags' => ['shipping']],
]);
```

Response là hội thoại sau khi cập nhật. Mỗi thay đổi được ghi thành một sự kiện trên dòng thời gian, đứng tên API key.

## 5. Nhận thông báo bằng webhook

Tạo endpoint webhook (dashboard **Webhooks**, hoặc `POST /webhooks`) và chọn các sự kiện Hỗ trợ cần nhận. Mỗi lần gửi có dạng `{ "type", "created_at", "data" }`, ký như mọi webhook khác của Hạ Tầng ([Webhook](https://hatang.dev/vi/docs/guides/getting-started/webhooks.md)).

| Sự kiện | Khi nào |
|---|---|
| `chat.conversation.created` | Có hội thoại mới trên bất kỳ kênh nào |
| `chat.conversation.assigned` | Người phụ trách thay đổi |
| `chat.conversation.resolved`, `chat.conversation.closed`, `chat.conversation.reopened` | Trạng thái thay đổi |
| `chat.conversation.handoff` | Trợ lý AI chuyển hội thoại cho người |
| `chat.conversation.tagged` | Có tag mới (mỗi tag mới một sự kiện, kèm `tag`) |
| `chat.message.created` | Có tin công khai mới, từ khách, nhân viên, bot hoặc API |
| `chat.visitor.identified` | Một khách truy cập được nhận diện |
| `chat.ticket.created`, `chat.ticket.updated`, `chat.ticket.resolved`, `chat.ticket.breached` | Phiếu hỗ trợ thay đổi, quá hạn SLA |
| `chat.rating.created` | Khách đánh giá hội thoại |
| `chat.order.created`, `chat.order.updated`, `chat.order.paid` | Đơn hàng |
| `chat.flow.webhook`, `chat.campaign.lead` | Bước webhook của kịch bản chạy; form chiến dịch thu được lead |
| `chat.zns.sent`, `chat.zns.failed` | Kết quả gửi Zalo ZNS |

`chat.message.created` mang nội dung tin cùng một object `conversation`. Ghi chú nội bộ không bắn sự kiện này.

> **Caution: Tránh vòng lặp trả lời**
>
> `chat.message.created` bắn cả cho tin bạn vừa gửi. Nếu bot trả lời theo sự kiện này, chỉ xử lý khi `data.sender_type` là `visitor`.

## 6. Liên kết tới trang quản trị của bạn

Liên kết hồ sơ đặt URL của bạn cạnh mỗi hội thoại, điền theo người trong hội thoại đó. `PUT /chat/profile-links` (scope `chat:write`) thay toàn bộ danh sách; `GET /chat/profile-links` (`chat:read`) trả về danh sách.

**cURL**

```bash
curl -X PUT "https://api.hatang.dev/chat/profile-links" \
  -H "Authorization: Bearer ht_123..." \
  -H "Content-Type: application/json" \
  -d '{"data": [{"label": "Admin", "url": "https://example.com/admin/users/{{user_id}}"}, {"label": "Orders", "url": "https://example.com/admin/orders?email={{email}}"}]}'
```

**Node.js**

```js
await hatang.chat.profileLinks.set([
  { label: "Admin", url: "https://example.com/admin/users/{{user_id}}" },
  { label: "Orders", url: "https://example.com/admin/orders?email={{email}}" },
]);
```

**Python**

```python
requests.put(
    "https://api.hatang.dev/chat/profile-links",
    headers={"Authorization": "Bearer ht_123..."},
    json={"data": [
        {"label": "Admin", "url": "https://example.com/admin/users/{{user_id}}"},
        {"label": "Orders", "url": "https://example.com/admin/orders?email={{email}}"},
    ]},
)
```

**PHP**

```php
$client->put('/chat/profile-links', [
    'headers' => ['Authorization' => 'Bearer ht_123...'],
    'json'    => ['data' => [
        ['label' => 'Admin', 'url' => 'https://example.com/admin/users/{{user_id}}'],
        ['label' => 'Orders', 'url' => 'https://example.com/admin/orders?email={{email}}'],
    ]],
]);
```

Phản hồi là `{ "object": "list", "data": [...] }`, mỗi liên kết có `id` (`pl_…`); gửi lại id để giữ nguyên liên kết. Thêm `site_ids` để chỉ hiện liên kết trong hội thoại của một số site.

| Biến | Giá trị |
|---|---|
| `{{user_id}}` | `user_id` đã xác minh từ `identify` có chữ ký; nếu không, là id khách tự khai và nhân viên thấy liên kết bị đánh dấu chưa xác minh ([Nhận diện khách](https://hatang.dev/vi/docs/guides/support/web-widget/identity-verification.md#liên-kết-hồ-sơ)) |
| `{{email}}`, `{{phone}}`, `{{name}}` | Thông tin liên hệ của khách |
| `{{customer_id}}`, `{{conversation_id}}`, `{{site_id}}` | Id trong Hạ Tầng |
| `{{field.<key>}}` | Một trường thông tin khách hàng |
| `{{data.<key>}}` | Dữ liệu tuỳ chỉnh của khách (luôn bị đánh dấu chưa xác minh) |

Quy tắc: chỉ `https://`, biến chỉ đứng sau tên miền (không bao giờ trong tên miền), mỗi URL tối đa 500 ký tự, tối đa 10 liên kết. Sai quy tắc trả `422 validation_error`. Giá trị được mã hoá URL, và liên kết có biến không có giá trị sẽ không hiện.

## Các endpoint khác

Cùng API này bao phủ phần còn lại của Hỗ trợ. Mọi đường dẫn bắt đầu bằng `https://api.hatang.dev/chat`:

| Nhóm | Đường dẫn |
|---|---|
| Site | `/sites`, `/sites/{id}`, `/sites/{id}/identity-secret` |
| Khách truy cập và bảng Đang truy cập | `/visitors/{id}`, `/visitors/{id}/conversations`, `/live`, `/live/greet` |
| Khách hàng | `/customers`, `/customers/{id}`, `/customers/{id}/merge`, `/customers/{id}/conversations`, `/customers/export.csv`, `/customer-fields` |
| Phiếu hỗ trợ và SLA | `/tickets`, `/tickets/stats`, `/tickets/{id}`, `/sla` |
| Liên kết hồ sơ | `/profile-links` |
| Tự động hoá | `/flows`, `/workflows`, `/campaigns`, `/routing-rules`, `/canned-responses` |
| Kiến thức cho AI | `/sites/{id}/kb/sources`, `/sites/{id}/kb/files`, `/sites/{id}/kb/search` |
| Bán hàng | `/products`, `/orders`, `/orders/{id}/payment-link` |
| Zalo ZNS | `/zns/templates`, `/zns`, `/zns/bulk` |
| Báo cáo | `/reports`, `/ratings` |

Mọi request và response có trong [tham chiếu REST API](https://hatang.dev/vi/docs/reference/api.md). SDK Node.js bọc phần lớn các endpoint này trong `hatang.chat.*`; xem [SDK Node.js](https://hatang.dev/vi/docs/guides/sdks/node.md).

## Lỗi và giới hạn

Lỗi dùng chung dạng `{ "statusCode", "name", "message" }`. Rẽ nhánh theo `name`, không theo `message`.

| Mã | `name` | Nguyên nhân |
|---|---|---|
| 401 | `missing_api_key` | Thiếu header `Authorization` |
| 403 | `invalid_api_key` | Key không tồn tại hoặc đã thu hồi |
| 401 | `restricted_api_key` | Key thiếu scope cho endpoint này |
| 403 | `product_disabled` | Module Hỗ trợ đang tắt |
| 404 | `not_found` | Id không tồn tại trong doanh nghiệp này |
| 409 | `conversation_closed` | Gửi tin vào hội thoại đã đóng |
| 422 | `validation_error` | Body hoặc query không hợp lệ |
| 429 | `rate_limit_exceeded` | Vượt giới hạn tần suất |

Các endpoint Hỗ trợ cho phép 50 request mỗi giây cho mỗi doanh nghiệp. Xem [Lỗi](https://hatang.dev/vi/docs/reference/errors.md) và [Giới hạn](https://hatang.dev/vi/docs/reference/limits.md).

## Bước tiếp theo

- [Khái niệm và luồng tin nhắn](https://hatang.dev/vi/docs/guides/support/overview.md)
- [Tự động hoá](https://hatang.dev/vi/docs/guides/support/automation.md): kịch bản, chu trình và quy tắc phân phối, cũng quản lý được qua API
- [Realtime](https://hatang.dev/vi/docs/guides/support/realtime.md): widget và ứng dụng nhân viên nói chuyện với gateway thế nào
- [Webhook](https://hatang.dev/vi/docs/guides/getting-started/webhooks.md): xác minh chữ ký và xử lý gửi lại

Source: https://hatang.dev/vi/docs/guides/support/rest-api
