Chat API 레퍼런스: 대화, 메시지, 읽음 확인과 이벤트
Hibira Chat API 호출 방법: 서명된 요청, 대화와 멤버, 메시지, 놓친 메시지 따라잡기, 읽음 확인과 안 읽은 수, 브라우저용 사용자 토큰, 첨부, 푸시, 모더레이션, 이벤트와 한도.
시작하기 전에
Chat API는 대화와 메시지를 저장하고, 앱이 이미 가진 실시간 연결로 전달합니다. 누가 어떤 대화에 있는지는 서버가 정하고 API를 호출합니다. 클라이언트는 연결하고 로그인한 뒤 받기만 합니다.
- 베타입니다. 계정이 베타에 들어 있거나 Chat 애드온을 쓰면 사용할 수 있습니다. 그 전에는 모든 호출이
403 the Chat API is not enabled for this app을 반환합니다. 대기자 명단에 등록하세요. - 기본 URL:
https://ws.hibira.io/apps/APP_ID/chat(포털의 앱 페이지에 표시된 호스트를 사용하세요). - 본문은 JSON입니다. 오류는 4xx 또는 5xx 상태와 함께
{"error": "..."}로 옵니다. - 사용자 ID는 직접 정합니다. 클라이언트가
pusher.signin()에 넘기는 것과 같은 ID를 쓰세요.
작동 방식
- 서버가 대화와 멤버를 만듭니다(
PUT /conversations/{id}). - 각 클라이언트는 pusher-js로 연결하고 자기 사용자로
pusher.signin()을 호출합니다. 그러면#server-to-user-<user_id>를 구독하므로, 대화마다 채널 인증이 필요 없습니다. - 서버가 사용자를 대신해 메시지를 보냅니다(
POST /conversations/{id}/messages). Hibira는 대화의 다음 번호(seq)로 저장한 뒤 모든 멤버에게chat.message를 보내고chat_message웹훅을 호출합니다. - 오프라인이었던 클라이언트는
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로는 이것이 전부입니다:
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 포함 |
대화:
{"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) | {}, 저장되지 않음 |
메시지:
{"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용 단기 토큰을 발급합니다.
첨부 파일
- 서버가 업로드를 요청합니다:
POST /conversations/{id}/attachments에user_id,name,content_type,size(최대 25 MB). 응답에는attachment와upload(url,method,headers)가 있습니다. - 클라이언트는 15분 안에 그 헤더 그대로
upload.url에 파일을 직접 올립니다. - 서버는
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} |
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명까지 늘릴 수 있습니다(요금제). 그달이 가득 차면 아직 세지 않은 사용자는 다음 달까지 보내기와 읽음에 429를 받고, 이미 센 사용자는 그대로 씁니다. GET /usage는 {month, active_users, limit}을 반환합니다.
메시지는 30일 동안 자유롭게 수정할 수 있습니다. 그 뒤로는 유료 요금제나 Chat 애드온이 있는 계정은 보관소에 두고 API가 계속(읽기 전용으로) 돌려주며, 애드온 없는 Free 요금제에서는 삭제됩니다.
AI 에이전트에서 사용하기
Hibira MCP 서버에는 Chat API 도구가 있어 AI 코딩 에이전트가 채팅 설정과 테스트를 대신할 수 있습니다: create_conversation, send_message, list_conversations, list_messages. 서명된 요청과 같은 검사로 내 앱에 작동하며, 계정에 Chat API가 켜져 있어야 합니다. Claude Code, Codex, Cursor 등을 위한 에이전트 플러그인에는 이 단계로 Chat API를 프로젝트에 연결하는 스킬이 있습니다. React나 순수 JavaScript용 기성 대화 화면은 @hibira-io/chat-ui를 보세요.