# Chat API 레퍼런스: 대화, 메시지, 읽음 확인과 이벤트

> Hibira Chat API 호출 방법: 서명된 요청, 대화와 멤버, 메시지, 놓친 메시지 따라잡기, 읽음 확인과 안 읽은 수, 브라우저용 사용자 토큰, 첨부, 푸시, 모더레이션, 이벤트와 한도.

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

## 시작하기 전에

Chat API는 대화와 메시지를 저장하고, 앱이 이미 가진 실시간 연결로 전달합니다. 누가 어떤 대화에 있는지는 서버가 정하고 API를 호출합니다. 클라이언트는 연결하고 로그인한 뒤 받기만 합니다.

- **베타입니다.** 계정이 베타에 들어 있거나 Chat 애드온을 쓰면 사용할 수 있습니다. 그 전에는 모든 호출이 `403 the Chat API is not enabled for this app`을 반환합니다. [대기자 명단에 등록](https://hibira.io/ko/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` 웹훅을 호출합니다.
4. 오프라인이었던 클라이언트는 `GET /conversations/{id}/messages?after=<가진 마지막 seq>`로 놓친 메시지를 읽습니다.

모든 변경은 이벤트를 보내기 전에 저장됩니다. 앱이 일일 메시지 한도를 넘으면 변경은 저장되지만 이벤트는 나가지 않으므로, 클라이언트는 읽어서 따라잡습니다.

## 요청 서명

서버의 요청은 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 채널로, 팀 받은편지함이 구독 하나로 모든 대화를 따라갈 수 있습니다. 직원에게만 인증해 주세요.

## 메시지

| 메서드와 경로 | 본문 또는 쿼리 | 반환 |
|---|---|---|
| `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`에서 `Authorization: Bearer <user token>`으로 Chat API를 직접 호출할 수 있어, 서버에 프록시가 필요 없습니다. 토큰은 앱 시크릿으로 서명한 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`입니다. 대화 만들기, 멤버 변경, 푸시 옵션 설정, 뮤트, 차단은 할 수 없고 서버에 남습니다. 사용자마다 초당 최대 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`(메시지당 최대 10개, 이때 `text`는 비워도 됩니다).

API나 이벤트에서 읽은 첨부에는 한 시간 동안 내려받을 수 있는 `url`이 있습니다. 하루 안에 보내지 않은 업로드는 지워지고, 메시지나 대화를 삭제하면 파일도 삭제됩니다.

## 오프라인 멤버에게 푸시

새 메시지는 앱의 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`를 반환하며 저장하지 않습니다. 차단된 사용자는 앱의 모든 대화에서 보내기, 수정, 반응, 입력 중 표시에 `403`을 받습니다. 뮤트된 멤버는 그 대화에서 보내기와 입력 중 표시에 `403`을 받지만 읽을 수는 있습니다.

## 이벤트와 웹훅

이벤트는 관련된 각 멤버의 `#server-to-user-<user_id>`와 대화의 watchers로 갑니다. 각각 일일 메시지 한도에 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` 웹훅도 옵니다. `channel`은 대화 ID, `user_id`는 보낸 사람, `data`는 JSON `{"conversation_id", "message"}`입니다. 받으려면 앱 웹훅의 이벤트에 `chat_message`를 추가하세요.

## 한도

| | |
|---|---|
| 대화 ID | `A-Z a-z 0-9 _ - = @ , . ;` 1–128자 |
| 사용자 ID | `pusher.signin` 사용자 ID와 같은 규칙 |
| 대화당 멤버 | 100 |
| 메시지 텍스트 | 4,000자, 공백만은 불가 |
| 대화 또는 보낸 사람 이름 | 100자 |
| `metadata` | JSON 객체, 최대 4,096바이트 |
| 한 번에 읽는 메시지 | 200 |
| 반응 | 각 1–32바이트, 메시지당 20종류 |
| 첨부 | 각 25 MB, 메시지당 10개 |

쓰기는 앱의 HTTP API 속도 제한을 함께 씁니다(넘으면 `429`).

## 월간 활성 사용자와 보관 기간

사용자는 한 달(UTC) 안에 메시지를 보내거나 대화를 읽음 처리하면 활성으로 치며, 계정의 모든 앱을 통틀어 한 번만 셉니다. 모든 계정에 월 500명이 포함되고, Chat 애드온으로 1,000, 5,000, 20,000, 60,000명까지 늘릴 수 있습니다([요금제](https://hibira.io/ko/chat)). 그달이 가득 차면 아직 세지 않은 사용자는 다음 달까지 보내기와 읽음에 `429`를 받고, 이미 센 사용자는 그대로 씁니다. `GET /usage`는 `{month, active_users, limit}`을 반환합니다.

메시지는 30일 동안 자유롭게 수정할 수 있습니다. 그 뒤로는 유료 요금제나 Chat 애드온이 있는 계정은 보관소에 두고 API가 계속(읽기 전용으로) 돌려주며, 애드온 없는 Free 요금제에서는 삭제됩니다.

## AI 에이전트에서 사용하기

[Hibira MCP 서버](https://hibira.io/ko/mcp-server)에는 Chat API 도구가 있어 AI 코딩 에이전트가 채팅 설정과 테스트를 대신할 수 있습니다: `create_conversation`, `send_message`, `list_conversations`, `list_messages`. 서명된 요청과 같은 검사로 내 앱에 작동하며, 계정에 Chat API가 켜져 있어야 합니다. Claude Code, Codex, Cursor 등을 위한 [에이전트 플러그인](https://hibira.io/ko/guides/agent-plugin)에는 이 단계로 Chat API를 프로젝트에 연결하는 스킬이 있습니다. React나 순수 JavaScript용 기성 대화 화면은 [@hibira-io/chat-ui](https://hibira.io/ko/guides/chat-ui)를 보세요.
