# Tài liệu tham khảo Chat API: cuộc trò chuyện, tin nhắn, xác nhận đã đọc và sự kiện

> Cách gọi Hibira Chat API: yêu cầu có chữ ký, cuộc trò chuyện và thành viên, tin nhắn, bắt kịp tin đã lỡ, xác nhận đã đọc và số chưa đọc, token người dùng cho trình duyệt, tệp đính kèm, push, kiểm duyệt, sự kiện và giới hạn.

Source: https://hibira.io/vi/chat-api

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

Chat API lưu các cuộc trò chuyện cùng tin nhắn và gửi chúng qua kết nối thời gian thực mà ứng dụng của bạn đã có. Máy chủ của bạn quyết định ai ở trong mỗi cuộc trò chuyện và gọi API; client chỉ kết nối, đăng nhập và lắng nghe.

- **Đang ở giai đoạn beta.** Tài khoản dùng được khi đã tham gia beta hoặc có gói bổ sung Chat. Trước đó mọi lời gọi đều trả về `403 the Chat API is not enabled for this app`. [Đăng ký danh sách chờ](https://hibira.io/vi/chat) để được tham gia.
- **URL gốc:** `https://ws.hibira.io/apps/APP_ID/chat` (dùng host hiển thị trên trang ứng dụng trong portal).
- **Nội dung** là JSON. Lỗi trả về `{"error": "..."}` với mã 4xx hoặc 5xx.
- **ID người dùng** do bạn đặt: dùng cùng ID mà client truyền cho `pusher.signin()`.

## Cách hoạt động

1. Máy chủ của bạn tạo cuộc trò chuyện và các thành viên (`PUT /conversations/{id}`).
2. Mỗi client kết nối bằng pusher-js và gọi `pusher.signin()` với tư cách người dùng của nó. Việc này đăng ký kênh `#server-to-user-<user_id>`, nên thành viên không cần ủy quyền kênh cho từng cuộc trò chuyện.
3. Máy chủ của bạn gửi tin nhắn thay người dùng (`POST /conversations/{id}/messages`). Hibira lưu nó với số thứ tự tiếp theo của cuộc trò chuyện (`seq`), rồi gửi `chat.message` tới mọi thành viên và gọi webhook `chat_message` của bạn.
4. Client từng offline đọc những gì đã lỡ bằng `GET /conversations/{id}/messages?after=<seq cuối cùng nó có>`.

Mọi thay đổi đều được lưu trước khi gửi sự kiện. Nếu ứng dụng vượt hạn mức tin nhắn hằng ngày, thay đổi vẫn được lưu nhưng không có sự kiện nào được gửi, nên client bắt kịp bằng cách đọc.

## Ký yêu cầu

Yêu cầu từ máy chủ của bạn được ký giống hệt các yêu cầu khác của Pusher HTTP API, bằng key và secret của ứng dụng: chuỗi truy vấn mang `auth_key`, `auth_timestamp`, `auth_version=1.0`, `body_md5` (khi có nội dung) và `auth_signature`, là HMAC-SHA256 của `METHOD\nPATH\nQUERY` với các tham số truy vấn sắp xếp theo tên. Mã ký của bất kỳ thư viện máy chủ Pusher nào cũng dùng được; đây là toàn bộ cách làm trong Node.js:

```js
import crypto from 'node:crypto';

const { PUSHER_APP_ID: id, PUSHER_APP_KEY: key, PUSHER_APP_SECRET: secret } = process.env;

export async function chat(method, path, body) {
  const fullPath = `/apps/${id}/chat${path}`;
  const json = body === undefined ? '' : JSON.stringify(body);
  const params = { auth_key: key, auth_timestamp: Math.floor(Date.now() / 1000), auth_version: '1.0' };
  if (json) params.body_md5 = crypto.createHash('md5').update(json).digest('hex');
  const query = Object.keys(params).sort().map((k) => `${k}=${params[k]}`).join('&');
  const signature = crypto.createHmac('sha256', secret).update(`${method}\n${fullPath}\n${query}`).digest('hex');
  const res = await fetch(`https://ws.hibira.io${fullPath}?${query}&auth_signature=${signature}`, {
    method, body: json || undefined, headers: { 'Content-Type': 'application/json' },
  });
  if (!res.ok) throw new Error(`${res.status} ${(await res.json()).error}`);
  return res.json();
}

await chat('PUT', '/conversations/order-1042', { name: 'Order #1042', members: ['ann', 'support-bob'] });
await chat('POST', '/conversations/order-1042/messages', { user_id: 'ann', name: 'Ann', text: 'Where is my parcel?' });
```

Tham số truy vấn của `GET` (như `after` hay `limit`) cũng nằm trong chuỗi truy vấn được ký.

## Cuộc trò chuyện và thành viên

| Phương thức và đường dẫn | Nội dung hoặc truy vấn | Trả về |
|---|---|---|
| `PUT /conversations/{id}` | `name`, `metadata`, `members` (ID người dùng; bỏ trống để giữ thành viên hiện tại), `roles` (`{user_id: role}`), `watchers` (tối đa 10 kênh private hoặc presence) | `201` khi tạo mới hoặc `200` khi thay thế, `{conversation}` |
| `GET /conversations/{id}` | | `{conversation}` |
| `DELETE /conversations/{id}` | | `{}`; xóa luôn thành viên, tin nhắn và tệp |
| `POST /conversations/{id}/members` | `user_ids` | `{conversation}` |
| `PUT /conversations/{id}/members/{user_id}` | `role` (1–32 ký tự `a-z 0-9 _ -`; `""` là không có) | `{conversation}` |
| `DELETE /conversations/{id}/members/{user_id}` | | `{conversation}` |
| `GET /users/{user_id}/conversations` | `limit` (1–100), `before` (`next` của trang trước) | `{conversations, next}`, hoạt động mới nhất trước, mỗi mục có `unread` |

Một cuộc trò chuyện:

```json
{"id": "order-1042", "name": "Order #1042", "metadata": {"kind": "support"},
 "members": [{"user_id": "ann", "read_seq": 3}, {"user_id": "support-bob", "read_seq": 1, "role": "agent"}],
 "last_seq": 3, "created_at": "...", "updated_at": "..."}
```

**Vai trò và ghi chú nội bộ.** Tin nhắn có `visible_to` (tối đa 5 vai trò) chỉ đến các thành viên có một trong các vai trò đó: những người khác không nhận, không đọc được và không tính là chưa đọc. **Watchers** là các kênh private hoặc presence nhận mọi sự kiện của cuộc trò chuyện, để hộp thư của đội theo dõi tất cả cuộc trò chuyện chỉ với một lần đăng ký. Chỉ ủy quyền cho nhân viên của bạn.

## Tin nhắn

| Phương thức và đường dẫn | Nội dung hoặc truy vấn | Trả về |
|---|---|---|
| `POST /conversations/{id}/messages` | `user_id` (một thành viên), `name`, `text`, `metadata`, `push`, `push_title`, `push_url`, `visible_to`, `attachments` | `201 {message, push}` |
| `GET /conversations/{id}/messages` | `after` (seq) và `limit`, hoặc `last` (n tin mới nhất) | `{messages, last_seq}`, cũ nhất trước |
| `GET /conversations/{id}/search` | `q` (1–100 ký tự), `before`, `limit` (1–200) | `{messages, next}`, mới nhất trước |
| `PUT /conversations/{id}/messages/{seq}` | `user_id` (người gửi), `text`, `metadata` | `{message}` có `edited_at` |
| `DELETE /conversations/{id}/messages/{seq}` | `user_id` (người gửi) trong truy vấn; bỏ trống để xóa với tư cách ứng dụng | `{message}` có `deleted_at` |
| `POST /conversations/{id}/messages/{seq}/reactions` | `user_id`, `emoji` | `{message}` |
| `DELETE /conversations/{id}/messages/{seq}/reactions` | `user_id`, `emoji` trong truy vấn | `{message}` |
| `POST /conversations/{id}/system` | `text`, `metadata` (ví dụ `{"event": "transferred", "to": "bob"}`) | `201 {message}` với `kind: "system"` |
| `POST /conversations/{id}/typing` | `user_id`, `typing` (`true` hoặc `false`) | `{}`; không lưu gì |

Một tin nhắn:

```json
{"id": "...", "seq": 3, "sender": "ann", "name": "Ann", "kind": "text",
 "text": "hi", "metadata": {"reply_to": 2}, "reactions": {"👍": ["support-bob"]}, "created_at": "..."}
```

Tin nhắn đã sửa giữ nguyên `seq` và có thêm `edited_at`. Tin đã xóa giữ chỗ của nó với `deleted_at` và không còn nội dung, không thể sửa hay thả cảm xúc nữa (`409`). Đọc `after` một số chỉ trả về tin mới hơn, nên client quay lại sẽ đọc lại các tin đang hiển thị (`last=n`) để cập nhật sửa, xóa và cảm xúc. Tìm kiếm khớp bất kỳ phần nào của từ, bỏ qua hoa thường và ký tự toàn chiều rộng, nên dùng được cho tiếng Trung, Nhật và Hàn. Tin hệ thống cho biết chuyện gì đã xảy ra (tham gia, chuyển giao, đóng): chỉ máy chủ của bạn gửi được, chúng không bao giờ được push hay tính là chưa đọc.

## Xác nhận đã đọc và số chưa đọc

| Phương thức và đường dẫn | Nội dung | Trả về |
|---|---|---|
| `POST /conversations/{id}/read` | `user_id`, `seq` | `{read_seq}` |

Vị trí đã đọc của thành viên chỉ tiến lên và không vượt quá `last_seq`; người gửi một tin coi như đã đọc nó. `unread` trong danh sách cuộc trò chuyện của người dùng đếm các tin sau `read_seq` của họ mà không phải tin hệ thống. Mọi thành viên nhận `chat.read` khi `read_seq` của ai đó tiến lên.

## Trình duyệt và ứng dụng: token người dùng

Client có thể tự gọi Chat API dưới `/apps/APP_ID/chat/me`, với `Authorization: Bearer <user token>` và không cần proxy trên máy chủ của bạn. Token là JWT ký bằng secret của ứng dụng (HS256, `{sub, exp}`, tối đa 30 ngày); máy chủ của bạn tạo nó bằng bất kỳ thư viện JWT nào hoặc bằng `POST /tokens`:

| Phương thức và đường dẫn | Nội dung | Trả về |
|---|---|---|
| `POST /tokens` | `user_id`, `conversation_id` (giới hạn token vào cuộc trò chuyện đó), `join` (thêm người dùng vào khi dùng lần đầu), `ttl` (60–86400 giây) | `201 {token, expires_at}` |

Có token, client có thể liệt kê cuộc trò chuyện của mình (`GET /me/conversations`), đọc, tìm kiếm, gửi, sửa và xóa tin của chính mình, thả cảm xúc, đánh dấu đã đọc và báo đang nhập. Người dùng luôn là `sub` của token. Client không thể tạo cuộc trò chuyện, đổi thành viên, đặt tùy chọn push, tắt tiếng hay cấm: những việc đó ở lại máy chủ của bạn. Mỗi người dùng ghi tối đa 2 lần mỗi giây, dồn tối đa 10 lần. Token có `conversation_id` và `join` phù hợp cho khách ẩn danh của chat hỗ trợ: máy chủ của bạn tạo cuộc trò chuyện, kiểm tra khách, rồi cấp token ngắn hạn cho một ID do nó tự tạo.

## Tệp đính kèm

1. Máy chủ của bạn xin tải lên: `POST /conversations/{id}/attachments` với `user_id`, `name`, `content_type` và `size` (tối đa 25 MB). Kết quả có `attachment` và `upload` (`url`, `method`, `headers`).
2. Client tải tệp thẳng lên `upload.url` trong vòng 15 phút, với đúng các header đó.
3. Máy chủ của bạn gửi tin nhắn kèm `attachments`: `id`, `name` và `content_type` từ bước 1 (tối đa 10 mỗi tin; khi đó `text` có thể để trống).

Mỗi tệp đính kèm đọc từ API hoặc sự kiện có một `url` tải xuống được trong một giờ. Tệp tải lên mà không được gửi trong một ngày sẽ bị xóa, và xóa tin nhắn hay cuộc trò chuyện sẽ xóa luôn tệp của nó.

## Push cho thành viên offline

Tin nhắn mới được push qua Hibira Push của ứng dụng tới các thành viên không kết nối (`push: "offline"`, mặc định), tới mọi thành viên trừ người gửi (`"always"`), hoặc không ai cả (`"never"`). Tiêu đề là tên người gửi và tên cuộc trò chuyện (hoặc `push_title`), nội dung là 180 ký tự đầu của tin, và `push_url` mở ra khi chạm vào. Thiết bị đăng ký bằng các SDK push tương thích Beams với cùng ID người dùng. `push` trong kết quả cho biết đã push bao nhiêu thành viên, hoặc vì sao không push.

## Kiểm duyệt

| Phương thức và đường dẫn | Nội dung hoặc truy vấn | Trả về |
|---|---|---|
| `PUT /moderation/filter` | `words` (tối đa 500), `action` (`mask` hoặc `reject`) | `{filter}` |
| `POST /users/{user_id}/ban` | `reason`, `until` (RFC 3339; bỏ trống là vô thời hạn) | `{user_id, banned, until}` |
| `DELETE /users/{user_id}/ban` | | `{user_id, banned}` |
| `POST /conversations/{id}/members/{user_id}/mute` | `reason`, `until` | `{conversation_id, user_id, muted, until}` |
| `DELETE /conversations/{id}/members/{user_id}/mute` | | `{conversation_id, user_id, muted}` |
| `POST /conversations/{id}/messages/{seq}/reports` | `user_id`, `reason` | `201 {report}` |
| `GET /moderation/reports` | `limit` (1–200) | `{reports}`, mới nhất trước |

Bộ lọc từ kiểm tra mọi tin mới hoặc đã sửa, khớp cả bên trong từ và bỏ qua hoa thường cùng ký tự toàn chiều rộng, nên bắt được cả tiếng Trung và tiếng Nhật. `mask` thay mỗi chỗ khớp bằng ＊; `reject` trả về `422` và không lưu gì. Người dùng bị cấm nhận `403` khi gửi, sửa, thả cảm xúc và báo đang nhập ở mọi cuộc trò chuyện của ứng dụng; thành viên bị tắt tiếng nhận `403` khi gửi và báo đang nhập ở cuộc trò chuyện đó nhưng vẫn đọc được.

## Sự kiện và webhook

Sự kiện được gửi tới `#server-to-user-<user_id>` của từng thành viên liên quan và tới watchers của cuộc trò chuyện. Mỗi sự kiện tính là một tin nhắn vào hạn mức hằng ngày.

| Sự kiện | Gửi tới | Dữ liệu |
|---|---|---|
| `chat.conversation` | mọi thành viên, khi tạo hoặc thay đổi, hoặc khi thêm thành viên | `{conversation}` |
| `chat.message` | mọi thành viên | `{conversation_id, message}` |
| `chat.message_updated` | mọi thành viên, sau khi sửa, xóa hoặc thả cảm xúc | `{conversation_id, message}` |
| `chat.typing` | mọi thành viên trừ người đang nhập | `{conversation_id, user_id, typing}` |
| `chat.read` | mọi thành viên | `{conversation_id, user_id, read_seq}` |
| `chat.left` | thành viên bị xóa khỏi cuộc trò chuyện | `{conversation_id}` |
| `chat.deleted` | mọi thành viên của cuộc trò chuyện bị xóa | `{conversation_id}` |

```js
const pusher = new Pusher('APP_KEY', { cluster: 'hkg', wsHost: 'ws.hibira.io', userAuthentication: { endpoint: '/pusher/user-auth' } });
pusher.signin();
pusher.user.bind('chat.message', ({ conversation_id, message }) => showMessage(conversation_id, message));
```

Máy chủ của bạn cũng nhận webhook `chat_message` cho mỗi tin mới: `channel` là ID cuộc trò chuyện, `user_id` là người gửi, và `data` là JSON `{"conversation_id", "message"}`. Thêm `chat_message` vào sự kiện của một webhook của ứng dụng để nhận nó.

## Giới hạn

| | |
|---|---|
| ID cuộc trò chuyện | 1–128 ký tự `A-Z a-z 0-9 _ - = @ , . ;` |
| ID người dùng | cùng quy tắc với ID người dùng của `pusher.signin` |
| Thành viên mỗi cuộc trò chuyện | 100 |
| Nội dung tin nhắn | 4.000 ký tự, không được trống |
| Tên cuộc trò chuyện hoặc người gửi | 100 ký tự |
| `metadata` | một đối tượng JSON, tối đa 4.096 byte |
| Tin nhắn mỗi lần đọc | 200 |
| Cảm xúc | mỗi cái 1–32 byte, 20 loại khác nhau mỗi tin |
| Tệp đính kèm | mỗi tệp 25 MB, 10 tệp mỗi tin |

Thao tác ghi dùng chung giới hạn tốc độ HTTP API của ứng dụng (`429` khi vượt).

## Người dùng hoạt động hằng tháng và thời gian lưu

Một người dùng hoạt động trong tháng (UTC) khi họ gửi tin hoặc đánh dấu đã đọc một cuộc trò chuyện, tính một lần trên mọi ứng dụng của tài khoản. Mỗi tài khoản có sẵn 500 người mỗi tháng; các gói bổ sung Chat nâng lên 1.000, 5.000, 20.000 hoặc 60.000 ([các gói](https://hibira.io/vi/chat)). Khi tháng đã đầy, người dùng chưa được tính nhận `429` khi gửi và đánh dấu đã đọc cho đến tháng sau; người đã được tính vẫn dùng bình thường. `GET /usage` trả về `{month, active_users, limit}`.

Tin nhắn sửa được hoàn toàn trong 30 ngày. Sau đó, tài khoản có gói trả phí hoặc gói bổ sung Chat giữ chúng trong kho lưu trữ mà API vẫn trả về (chỉ đọc); gói Free không có gói bổ sung thì chúng bị xóa.

## Dùng từ một AI agent

[Máy chủ MCP của Hibira](https://hibira.io/vi/mcp-server) có các công cụ Chat API, để một AI agent lập trình thiết lập và thử chat giúp bạn: `create_conversation`, `send_message`, `list_conversations` và `list_messages`. Chúng hoạt động trên chính các ứng dụng của bạn với cùng kiểm tra như một yêu cầu có chữ ký, và cần tài khoản đã có Chat API. [Plugin cho agent](https://hibira.io/vi/guides/agent-plugin) dành cho Claude Code, Codex, Cursor và các công cụ khác có một skill nối Chat API vào dự án theo các bước này. Nếu cần giao diện trò chuyện dựng sẵn cho React hoặc JavaScript thuần, xem [@hibira-io/chat-ui](https://hibira.io/vi/guides/chat-ui).
