# Chat API リファレンス：会話、メッセージ、既読とイベント

> Hibira Chat API の呼び出し方：署名付きリクエスト、会話とメンバー、メッセージ、取りこぼしの取得、既読と未読数、ブラウザ向けユーザートークン、添付、プッシュ、モデレーション、イベント、上限。

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

## はじめに

Chat API は会話とメッセージを保存し、アプリがすでに持っているリアルタイム接続で届けます。誰がどの会話に参加するかはあなたのサーバーが決めて API を呼び出し、クライアントは接続してサインインし、受け取るだけです。

- **ベータ版です。** アカウントがベータに参加しているか、Chat アドオンを契約していれば使えます。それまではすべての呼び出しが `403 the Chat API is not enabled for this app` を返します。[ウェイトリストに登録](https://hibira.io/ja/chat)してください。
- **ベース URL：** `https://ws.hibira.io/apps/APP_ID/chat`（ポータルのアプリページに表示されるホストを使います）。
- **ボディ**は 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>` で取りこぼしを読みます。

変更はすべて、イベントを送る前に保存されます。アプリが 1 日のメッセージ上限を超えていても変更は保存され、イベントは送られません。クライアントは読み込みで追いつきます。

## リクエストの署名

サーバーからのリクエストは、Pusher HTTP API のほかのリクエストと同じくアプリのキーとシークレットで署名します。クエリ文字列に `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`（private または presence チャンネル最大 10 個） | 新規は `201`、置き換えは `200`、`{conversation}` |
| `GET /conversations/{id}` | | `{conversation}` |
| `DELETE /conversations/{id}` | | `{}`。メンバー、メッセージ、ファイルも削除 |
| `POST /conversations/{id}/members` | `user_ids` | `{conversation}` |
| `PUT /conversations/{id}/members/{user_id}` | `role`（`a-z 0-9 _ -` の 1〜32 文字。`""` でなし） | `{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 チャンネルで、チームの受信箱が 1 つの購読ですべての会話を追えます。認可はスタッフだけにしてください。

## メッセージ

| メソッドとパス | ボディまたはクエリ | 戻り値 |
|---|---|---|
| `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`（送信者）。省略するとアプリとして削除 | `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` が届きます。

## ブラウザとアプリ：ユーザートークン

クライアントは `/apps/APP_ID/chat/me` で Chat API を直接呼べます。`Authorization: Bearer <user token>` を付ければ、サーバーにプロキシは要りません。トークンはアプリのシークレットで署名した JWT（HS256、`{sub, exp}`、最長 30 日）で、任意の JWT ライブラリか `POST /tokens` で作ります：

| メソッドとパス | ボディ | 戻り値 |
|---|---|---|
| `POST /tokens` | `user_id`、`conversation_id`（その会話に限定）、`join`（初回利用時にユーザーを会話に追加）、`ttl`（60〜86400 秒） | `201 {token, expires_at}` |

トークンがあれば、クライアントは自分の会話一覧（`GET /me/conversations`）、読み込み、検索、送信、自分のメッセージの編集と削除、リアクション、既読、入力中の通知ができます。ユーザーは常にトークンの `sub` です。会話の作成、メンバー変更、プッシュ設定、ミュート、BAN はできず、サーバー側に残ります。各ユーザーの書き込みは毎秒 2 回まで、バーストで 10 回までです。`conversation_id` と `join` 付きのトークンはサポートチャットの匿名訪問者に向いています。サーバーが会話を作り、訪問者を確認し、自分で決めた ID 用の短命トークンを発行します。

## 添付ファイル

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`（1 メッセージ最大 10 個。この場合 `text` は空でも構いません）。

API やイベントで読んだ添付にはそれぞれ 1 時間ダウンロードできる `url` が付きます。1 日以内に送られなかったアップロードは消え、メッセージや会話を削除するとファイルも削除されます。

## オフラインのメンバーへのプッシュ

新しいメッセージはアプリの 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}`、新しい順 |

NG ワードフィルターは新規・編集されたメッセージをすべて調べ、単語の途中でも、大文字小文字と全角半角を区別せずに一致するので、日本語や中国語にも効きます。`mask` は一致した部分を＊に置き換え、`reject` は `422` を返して保存しません。BAN されたユーザーはアプリのすべての会話で送信、編集、リアクション、入力中に `403` を受けます。ミュートされたメンバーはその会話で送信と入力中に `403` を受けますが、読むことはできます。

## イベントと Webhook

イベントは関係する各メンバーの `#server-to-user-<user_id>` と、会話の watchers に届きます。1 件ごとに 1 日のメッセージ数に 1 件として数えます。

| イベント | 宛先 | データ |
|---|---|---|
| `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"}` です。受け取るには、アプリの Webhook のイベントに `chat_message` を加えます。

## 上限

| | |
|---|---|
| 会話 ID | `A-Z a-z 0-9 _ - = @ , . ;` の 1〜128 文字 |
| ユーザー ID | `pusher.signin` のユーザー ID と同じルール |
| 1 会話のメンバー | 100 |
| メッセージ本文 | 4,000 文字、空白のみは不可 |
| 会話名・送信者名 | 100 文字 |
| `metadata` | JSON オブジェクト、最大 4,096 バイト |
| 1 回に読めるメッセージ | 200 |
| リアクション | 1 個 1〜32 バイト、1 メッセージ 20 種類まで |
| 添付ファイル | 1 個 25 MB、1 メッセージ 10 個 |

書き込みはアプリの HTTP API レート制限を共有します（超えると `429`）。

## 月間アクティブユーザーと保存期間

ユーザーはある月（UTC）にメッセージを送るか会話を既読にするとアクティブになり、アカウントの全アプリで 1 人と数えます。どのアカウントにも月 500 人が含まれ、Chat アドオンで 1,000、5,000、20,000、60,000 人に増やせます（[プラン](https://hibira.io/ja/chat)）。その月の上限に達すると、まだ数えられていないユーザーは翌月まで送信と既読で `429` を受けます。すでに数えられたユーザーはそのまま使えます。`GET /usage` は `{month, active_users, limit}` を返します。

メッセージは 30 日間すべて編集できます。その後、有料プランか Chat アドオンのあるアカウントではアーカイブに保管され、API は引き続き（読み取り専用で）返します。アドオンのない Free プランでは削除されます。

## AI エージェントから使う

[Hibira MCP サーバー](https://hibira.io/ja/mcp-server)には Chat API のツールがあり、AI コーディングエージェントがチャットの設定とテストを代わりに行えます：`create_conversation`、`send_message`、`list_conversations`、`list_messages`。署名付きリクエストと同じチェックであなた自身のアプリに対して動き、アカウントで Chat API が有効になっている必要があります。Claude Code、Codex、Cursor などに対応した[エージェントプラグイン](https://hibira.io/ja/guides/agent-plugin)には、この手順で Chat API をプロジェクトに組み込むスキルがあります。React や素の JavaScript 向けの既製の会話ビューは [@hibira-io/chat-ui](https://hibira.io/ja/guides/chat-ui) をご覧ください。
