# Chat API 參考文件：對話、訊息、已讀回條與事件

> 如何呼叫 Hibira Chat API：簽章請求、對話與成員、訊息、補齊漏掉的訊息、已讀回條與未讀數、給瀏覽器用的使用者 Token、附件、推播、內容管理、事件與限制。

Source: https://hibira.io/zh-tw/chat-api

## 開始之前

Chat API 會儲存對話與其中的訊息，並透過你的 App 已經建立的即時連線送達。由你的伺服器決定每個對話有哪些人並呼叫 API；用戶端只需要連線、登入並接收。

- **目前為 Beta。** 帳號加入 Beta 或購買 Chat 加購方案後即可使用。在此之前，每個請求都會回應 `403 the Chat API is not enabled for this app`。[加入候補名單](https://hibira.io/zh-tw/chat)即可申請。
- **Base URL：** `https://ws.hibira.io/apps/APP_ID/chat`（請使用入口網站 App 頁面上顯示的主機）。
- **內容**為 JSON。錯誤以 4xx 或 5xx 狀態碼回應 `{"error": "..."}`。
- **使用者 ID** 由你決定：使用與用戶端傳給 `pusher.signin()` 相同的 ID。

## 運作方式

1. 你的伺服器建立對話及其成員（`PUT /conversations/{id}`）。
2. 每個用戶端用 pusher-js 連線，並以自己的使用者身分呼叫 `pusher.signin()`。這會訂閱 `#server-to-user-<user_id>`，所以成員不需要為每個對話做頻道授權。
3. 你的伺服器代替使用者發送訊息（`POST /conversations/{id}/messages`）。Hibira 會以對話的下一個序號（`seq`）儲存，接著把 `chat.message` 送給每位成員，並呼叫你的 `chat_message` Webhook。
4. 離線過的用戶端用 `GET /conversations/{id}/messages?after=<手上最後的 seq>` 讀回漏掉的訊息。

每個變更都會先儲存，再送出事件。如果 App 超過每日訊息額度，變更仍會儲存但不會送出事件，用戶端可以讀取補齊。

## 簽章請求

來自你伺服器的請求，與 Pusher HTTP API 的其他請求一樣以 App key 與 secret 簽章：查詢字串帶有 `auth_key`、`auth_timestamp`、`auth_version=1.0`、`body_md5`（有內容時）與 `auth_signature`，也就是對 `METHOD\nPATH\nQUERY`（查詢參數依名稱排序）計算的 HMAC-SHA256。任何 Pusher 伺服器函式庫的簽章程式碼都能用；以下是 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?' });
```

`GET` 的查詢參數（例如 `after` 或 `limit`）也包含在簽章的查詢字串中。

## 對話與成員

| 方法與路徑 | 內容或查詢參數 | 回應 |
|---|---|---|
| `PUT /conversations/{id}` | `name`、`metadata`、`members`（使用者 ID；省略則保留現有成員）、`roles`（`{user_id: role}`）、`watchers`（最多 10 個 private 或 presence 頻道） | 新建 `201`、取代 `200`，`{conversation}` |
| `GET /conversations/{id}` | | `{conversation}` |
| `DELETE /conversations/{id}` | | `{}`；一併刪除成員、訊息與檔案 |
| `POST /conversations/{id}/members` | `user_ids` | `{conversation}` |
| `PUT /conversations/{id}/members/{user_id}` | `role`（1–32 個 `a-z 0-9 _ -`；`""` 表示無） | `{conversation}` |
| `DELETE /conversations/{id}/members/{user_id}` | | `{conversation}` |
| `GET /users/{user_id}/conversations` | `limit`（1–100）、`before`（上一頁的 `next`） | `{conversations, next}`，依最新動態排序，每筆含 `unread` |

對話：

```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": "..."}
```

**角色與內部備註。** 帶有 `visible_to`（最多 5 個角色）的訊息只會送到擁有其中一個角色的成員：其他人不會收到、讀到，也不會算入未讀。**Watchers** 是會收到該對話所有事件的 private 或 presence 頻道，讓團隊收件匣用一個訂閱追蹤所有對話。請只授權你的員工訂閱。

## 訊息

| 方法與路徑 | 內容或查詢參數 | 回應 |
|---|---|---|
| `POST /conversations/{id}/messages` | `user_id`（成員）、`name`、`text`、`metadata`、`push`、`push_title`、`push_url`、`visible_to`、`attachments` | `201 {message, push}` |
| `GET /conversations/{id}/messages` | `after`（seq）與 `limit`，或 `last`（最新 n 則） | `{messages, last_seq}`，由舊到新 |
| `GET /conversations/{id}/search` | `q`（1–100 字元）、`before`、`limit`（1–200） | `{messages, next}`，由新到舊 |
| `PUT /conversations/{id}/messages/{seq}` | `user_id`（發送者）、`text`、`metadata` | 含 `edited_at` 的 `{message}` |
| `DELETE /conversations/{id}/messages/{seq}` | 查詢參數 `user_id`（發送者）；省略則以 App 身分刪除 | 含 `deleted_at` 的 `{message}` |
| `POST /conversations/{id}/messages/{seq}/reactions` | `user_id`、`emoji` | `{message}` |
| `DELETE /conversations/{id}/messages/{seq}/reactions` | 查詢參數 `user_id`、`emoji` | `{message}` |
| `POST /conversations/{id}/system` | `text`、`metadata`（例如 `{"event": "transferred", "to": "bob"}`） | `kind: "system"` 的 `201 {message}` |
| `POST /conversations/{id}/typing` | `user_id`、`typing`（`true` 或 `false`） | `{}`；不會儲存 |

訊息：

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

編輯過的訊息保留 `seq` 並加上 `edited_at`。刪除的訊息保留位置、帶 `deleted_at` 且沒有文字，之後不能再編輯或表情回應（`409`）。`after` 只回傳更新的訊息，所以回到線上的用戶端要重新讀取畫面上的訊息（`last=n`），才能取得編輯、刪除與表情回應。搜尋會比對字詞的任何部分，忽略大小寫與全形，所以中文、日文、韓文都適用。系統訊息說明發生了什麼（加入、轉接、結案）：只有你的伺服器能發送，不會推播，也不會算入未讀。

## 已讀回條與未讀數

| 方法與路徑 | 內容 | 回應 |
|---|---|---|
| `POST /conversations/{id}/read` | `user_id`、`seq` | `{read_seq}` |

成員的已讀位置只會前進，且不超過 `last_seq`；訊息的發送者視為已讀。使用者對話列表中的 `unread` 計算 `read_seq` 之後、非系統訊息的數量。有人的 `read_seq` 前進時，每位成員都會收到 `chat.read`。

## 瀏覽器與 App：使用者 Token

用戶端可以自己在 `/apps/APP_ID/chat/me` 底下呼叫 Chat API，帶上 `Authorization: Bearer <user token>`，你的伺服器不需要代理。Token 是用 App secret 簽署的 JWT（HS256、`{sub, exp}`、最長 30 天）；你的伺服器可用任何 JWT 函式庫或 `POST /tokens` 產生：

| 方法與路徑 | 內容 | 回應 |
|---|---|---|
| `POST /tokens` | `user_id`、`conversation_id`（只限該對話）、`join`（第一次使用時把使用者加入對話）、`ttl`（60–86400 秒） | `201 {token, expires_at}` |

有了 Token，用戶端可以列出自己的對話（`GET /me/conversations`）、讀取、搜尋、發送、編輯與刪除自己的訊息、加上表情回應、標記已讀與送出輸入中狀態。使用者一律是 Token 的 `sub`。用戶端不能建立對話、變更成員、設定推播選項、禁言或封鎖：這些都留在你的伺服器。每位使用者每秒最多寫入 2 次，突發最多 10 次。帶 `conversation_id` 與 `join` 的 Token 適合客服聊天的匿名訪客：你的伺服器建立對話、驗證訪客，再為自訂的 ID 發出短效 Token。

## 附件

1. 你的伺服器申請上傳：`POST /conversations/{id}/attachments`，帶 `user_id`、`name`、`content_type` 與 `size`（最大 25 MB）。回應含 `attachment` 與 `upload`（`url`、`method`、`headers`）。
2. 用戶端在 15 分鐘內，用完全相同的標頭把檔案直接上傳到 `upload.url`。
3. 你的伺服器發送帶 `attachments` 的訊息：第 1 步取得的 `id`、`name` 與 `content_type`（每則最多 10 個；此時 `text` 可以留空）。

從 API 或事件讀到的每個附件都帶有一小時內可下載的 `url`。一天內沒有送出的上傳會被移除，刪除訊息或對話也會刪除其檔案。

## 推播給離線成員

新訊息會透過 App 的 Hibira Push 推播給未連線的成員（`push: "offline"`，預設）、除發送者外的所有成員（`"always"`），或不推播（`"never"`）。標題是發送者與對話名稱（或 `push_title`），內文是文字的前 180 個字元，點擊時開啟 `push_url`。裝置以相容 Beams 的推播 SDK 用相同的使用者 ID 註冊。回應中的 `push` 會說明推播了幾位成員，或為什麼沒有推播。

## 內容管理

| 方法與路徑 | 內容或查詢參數 | 回應 |
|---|---|---|
| `PUT /moderation/filter` | `words`（最多 500 個）、`action`（`mask` 或 `reject`） | `{filter}` |
| `POST /users/{user_id}/ban` | `reason`、`until`（RFC 3339；省略表示無期限） | `{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}`，由新到舊 |

詞語過濾會檢查每則新的或編輯過的訊息，比對字詞內部並忽略大小寫與全形，所以中文與日文也能攔截。`mask` 把每個符合處換成＊；`reject` 回應 `422` 且不儲存。被封鎖的使用者在 App 的所有對話中發送、編輯、表情回應與輸入中都會得到 `403`；被禁言的成員在該對話中發送與輸入中會得到 `403`，但仍可讀取。

## 事件與 Webhook

事件會送到相關成員各自的 `#server-to-user-<user_id>`，以及對話的 watchers。每個事件算一則訊息額度。

| 事件 | 對象 | 資料 |
|---|---|---|
| `chat.conversation` | 每位成員，於建立、變更或新增成員時 | `{conversation}` |
| `chat.message` | 每位成員 | `{conversation_id, message}` |
| `chat.message_updated` | 每位成員，於編輯、刪除或表情回應之後 | `{conversation_id, message}` |
| `chat.typing` | 輸入者以外的每位成員 | `{conversation_id, user_id, typing}` |
| `chat.read` | 每位成員 | `{conversation_id, user_id, read_seq}` |
| `chat.left` | 被移除的成員 | `{conversation_id}` |
| `chat.deleted` | 被刪除對話的每位成員 | `{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));
```

每則新訊息也會呼叫你伺服器的 `chat_message` Webhook：`channel` 是對話 ID，`user_id` 是發送者，`data` 是 JSON `{"conversation_id", "message"}`。在 App 的某個 Webhook 事件中加入 `chat_message` 即可接收。

## 限制

| | |
|---|---|
| 對話 ID | 1–128 個 `A-Z a-z 0-9 _ - = @ , . ;` |
| 使用者 ID | 與 `pusher.signin` 使用者 ID 規則相同 |
| 每個對話的成員 | 100 |
| 訊息文字 | 4,000 字元，不可空白 |
| 對話或發送者名稱 | 100 字元 |
| `metadata` | JSON 物件，最多 4,096 bytes |
| 每次讀取的訊息 | 200 |
| 表情回應 | 每個 1–32 bytes，每則訊息最多 20 種 |
| 附件 | 每個 25 MB，每則訊息 10 個 |

寫入共用 App 的 HTTP API 速率限制（超過時回應 `429`）。

## 每月活躍使用者與保存期間

使用者在某個月（UTC）發送訊息或標記對話已讀，即為該月的活躍使用者，帳號下所有 App 只計算一次。每個帳號每月包含 500 位；Chat 加購方案可提高到 1,000、5,000、20,000 或 60,000 位（[方案](https://hibira.io/zh-tw/chat)）。當月額滿後，尚未計入的使用者在發送與已讀時會得到 `429`，直到下個月；已計入的使用者不受影響。`GET /usage` 回應 `{month, active_users, limit}`。

訊息在 30 天內可完整編輯。之後，付費方案或有 Chat 加購方案的帳號會將訊息保存在封存中，API 仍會回傳（唯讀）；沒有加購方案的 Free 方案則會刪除。

## 從 AI 代理使用

[Hibira MCP 伺服器](https://hibira.io/zh-tw/mcp-server)提供 Chat API 工具，讓 AI 程式代理幫你設定與測試聊天：`create_conversation`、`send_message`、`list_conversations` 與 `list_messages`。它們作用在你自己的 App 上，檢查與簽章請求相同，且帳號需要已開通 Chat API。適用 Claude Code、Codex、Cursor 等工具的[代理外掛](https://hibira.io/zh-tw/guides/agent-plugin)提供一個技能，依照這些步驟把 Chat API 接入專案。若需要 React 或純 JavaScript 的現成對話介面，請見 [@hibira-io/chat-ui](https://hibira.io/zh-tw/guides/chat-ui)。
