Korat

손님에게 답하는 채팅 봇 만들기

이 문서는 코드를 짜지 않는 사장님도 먼저 따라 할 수 있게 쓰고, 개발자를 위한 기술 상세는 페이지 끝에 두었습니다 — 바로 가고 싶은 곳이 있다면 오른쪽 목차를 쓰세요

봇이란 무엇이고 무엇을 할 수 있나

봇은 화면 뒤에 사람이 앉아 타자를 치지 않는, Korat 시스템 안의 또 다른 종류의 "사용자 계정"입니다 — 여러분의 프로그램이 요청을 쏘면 봇이 초대된 채팅방에 메시지가 나타납니다. 실제로 이렇게 씁니다.

  • 새 주문을 채팅으로 알리기 — 가게 판매 시스템에 새 주문이 들어오면 ⇒ 봇이 손님과의 채팅방이나 직원 방에 곧바로 알림을 씁니다
  • 재고가 곧 떨어진다고 알리기 — 재고가 정해 둔 수량보다 적어지면 ⇒ 봇이 가게 직원 방에 경고를 씁니다
  • 서버·시스템 상태 알리기 — 여러분이 만든 상태 점검 스크립트가 끝나면 ⇒ 봇이 IT 팀이 보는 그룹 방에 결과를 보고합니다

봇이 할 수 없는 일(임시 제약이 아니라 데이터베이스에서 온 사실입니다):

  • 봇은 방의 메시지를 읽을 수 없습니다 — 나가는 쪽 전용입니다. 여러분이 직접 수신 웹훅을 설정한 경우는 예외입니다(페이지 끝의 기술 레퍼런스를 보세요). 다만 웹훅을 설정해 두어도 오늘은 실제로 아무것도 보내지지 않습니다. 큐를 끌어다 쏘는 시계가 아직 프로덕션에 설정되지 않았기 때문입니다
  • 봇은 스스로 방에 들어올 수 없습니다 — 이미 그 방에 있는 사람(또는 그 방을 관리할 권한이 있는 사람)이 언제나 초대해야 합니다
  • 봇 하나가 동시에 쓸 수 있는 토큰은 2장까지이며, 계정 하나가 만들 수 있는 봇은 최대 5개입니다

어떻게 시작하나 — 4단계

  1. 1봇 만들기. 봇의 이름과 아이디를 정합니다(아이디는 bot으로 끝나야 합니다. 예: orderbot) — 여러분 개인이 아니라 가게의 봇으로 두고 싶다면 만들 때 가게를 고르면 됩니다
  2. 2토큰 복사하기. 만들기가 끝나면 시스템이 토큰을 딱 한 번 보여 줍니다 — 곧바로 안전한 곳에 복사해 두세요. 화면을 닫으면 실제 값을 다시 꺼내 볼 방법이 없습니다(새로 발급받아야 합니다)
  3. 3봇을 방에 초대하기. 봇이 말하게 할 채팅방, 그룹 방, 직원 방을 고릅니다 — 여러분이 이미 그 방에 있거나 그 방을 관리할 권한이 있어야 초대할 수 있습니다
  4. 4첫 메시지 쏘기. 2단계의 토큰으로 Bot API를 호출합니다 — 그대로 따라 할 수 있는 예제가 다음 항목에 있습니다

1~3단계는 Korat 앱에서 합니다

메뉴는 설정 › 계정 › "내 봇"에 있습니다 — 거기서 봇을 만들고, 토큰을 발급·교체·폐기하고, 채팅방·그룹 방·직원 방에 봇을 초대하는 일을 코드 한 줄 없이 모두 할 수 있습니다. 이 메뉴는 다음 Korat 앱 버전에 들어갑니다 — 오늘 내려받아 쓰는 버전에는 아직 없습니다. 설정을 열었는데 "내 봇"이 보이지 않으면 앱 업데이트를 기다리세요. 그동안에는 Supabase의 RPC를 직접 불러 1~3단계를 똑같이 할 수 있습니다(페이지 끝의 기술 레퍼런스를 보세요)

🔒 토큰은 봇의 비밀번호입니다

이 토큰을 가진 사람은 누구든 곧바로 봇의 이름으로 메시지를 보낼 수 있습니다 — 채팅, 사진, 암호화되지 않은 이메일로 토큰을 보내지 마세요. 여러분의 서버나 비밀 저장소에만 두세요. Korat 직원은 누구도 토큰을 요구하지 않습니다. 채팅이든 이메일이든 전화든 마찬가지입니다 — Korat 팀이라고 하면서 토큰을 요구하는 사람이 있다면 사기로 여기고 문의로 즉시 알려 주세요. 토큰이 새어 나갔다면 앱에서 그 장을 곧바로 폐기할 수 있습니다(되돌릴 수 없으며 새로 발급받아야 합니다)

예제 — 첫 메시지 쏘기

엔드포인트는 POST https://koratland.com/api/bot/send 하나뿐입니다. 요청 헤더에 토큰을 넣고, 어느 방에 무엇을 말할지 알려 주세요.

curl로 하기 (터미널이 익숙한 분께)

curl -X POST "https://koratland.com/api/bot/send" \
  -H "X-Bot-Token: kbot_12_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "kind": "chat", "chat_id": 9931, "text": "새 주문이 들어왔습니다" }'

kbot_12_xxxxxxxxxxxxxxxxxxxx는 봇의 실제 토큰으로, 9931은 봇을 초대해 둔 방의 번호로 바꾸세요(방 번호는 앱의 채팅 링크나 "내 봇" 화면의 방 목록에서 볼 수 있습니다).

코드를 짜지 않고 하기 (Google Apps Script)

자기 서버가 없다면 구글 계정 하나로 Google Apps Script를 무료로 쓸 수 있습니다 — script.google.com에 들어가 → 새 프로젝트 → 이 코드를 붙여 넣고 → "실행"을 누릅니다. 처음 실행할 때 인터넷 접근 권한을 묻습니다(여러분 자신의 스크립트이므로 허용해도 안전합니다).

function sendKoratMessage() {
  var url = "https://koratland.com/api/bot/send";
  var payload = {
    kind: "chat",
    chat_id: 9931,              // 여러분의 방 번호로 바꾸세요
    text: "새 주문이 들어왔습니다"   // 보내고 싶은 메시지로 바꾸세요
  };
  var options = {
    method: "post",
    contentType: "application/json",
    headers: { "X-Bot-Token": "kbot_12_xxxxxxxxxxxxxxxxxxxx" },  // 여러분의 봇 토큰으로 바꾸세요
    payload: JSON.stringify(payload)
  };
  var res = UrlFetchApp.fetch(url, options);
  Logger.log(res.getContentText());  // 결과는 "실행 로그"(Execution log)에서 봅니다
}

"실행"을 한 번 누르면 곧바로 시험해 볼 수 있습니다 — 자동으로 쏘게 하고 싶다면, 예를 들어 주문을 적는 Google Sheet에 새 행이 생길 때마다 쏘려면, Apps Script의 트리거(Triggers)로 시트가 수정될 때(onEdit)나 몇 분마다 이 함수를 부르도록 두면 됩니다.

같은 방법을 "HTTP request"나 "Webhook" 동작이 이미 있는 자동화 도구에도 그대로 쓸 수 있습니다. 예를 들어 n8n, Make, Zapier — 같은 URL로 POST를 보내도록 설정하고 X-Bot-Token 헤더와 위 예제와 같은 JSON 본문을 넣으세요.

자주 겪는 문제

이렇게 나온다면 (reason)무슨 뜻인가어떻게 고치나
not_invited봇이 아직 이 방에 초대되지 않았습니다(또는 초대된 적이 있으나 쫓겨났습니다)"내 봇" 화면이나 bot_invite로 봇을 그 방에 다시 초대하세요
invite_stale봇을 이 방에 초대한 사람이 방에서의 권한을 잃었습니다(그룹에서 나갔거나, 역할이 내려갔거나, 직원 방 관리 권한을 빼앗겼거나, 방이 닫혔습니다) ⇒ 아무도 쫓아내지 않았는데 봇이 스스로 조용해집니다그 방에 아직 실제로 권한이 있는 사람이 봇을 다시 초대하게 하세요 — 권한은 언제나 가장 최근에 초대한 사람을 기준으로 계산됩니다
conversation_not_open이 방은 가게의 메시지함이고 손님이 아직 한 번도 말을 건 적이 없습니다 — 가게 쪽(가게의 봇을 포함해)은 손님에게 먼저 말을 걸 수 없습니다손님이 먼저 말을 걸 때까지 기다린 뒤에 봇이 답하게 하세요
rate_limited봇이 정해 둔 한도(분당 또는 하루)보다 자주 메시지를 쐈습니다retry_after_sec에 적힌 시간만큼 기다렸다가 다시 쏘세요 — 더 높은 한도가 꼭 필요하다면 문의로 팀에 알려 주세요
blocked그 채팅방의 사용자가 이 봇을 차단해 두었습니다사용자 쪽에서 고칠 수 없습니다 — 꼭 필요하다면 다른 방이나 다른 봇을 쓰세요
잘 말하던 봇이 갑자기 아무 이유 없이 조용해짐가장 흔한 원인은 위의 invite_stale입니다 — 봇이 망가진 것이 아닙니다봇을 그 방에 초대한 사람이 아직 방에 있는지, 권한이 남아 있는지 확인하세요

모든 오류 코드는 아래 기술 레퍼런스의 표에서 볼 수 있습니다

기술 레퍼런스 (개발자용)

아래 항목은 Bot API를 직접 호출하는 프로그램을 짤 사람을 위한 것입니다 — 모든 파라미터의 상세, 전체 오류 코드 표, 그리고 수신 웹훅.

1~3단계를 RPC로 직접 하기 (앱 없이)

쓰고 있는 앱 버전에 "내 봇" 화면이 아직 없거나 자동으로 처리하고 싶다면, 로그인한 사용자의 Access Token(봇 토큰이 아닙니다)으로 PostgREST를 통해 Supabase의 RPC를 직접 부르세요 — 세 단계입니다.

1. 봇 만들기 — bot_create

curl -X POST "https://<SUPABASE_PROJECT>.supabase.co/rest/v1/rpc/bot_create" \
  -H "apikey: <SUPABASE_ANON_KEY>" \
  -H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "p_name": "알림봇",
    "p_username": "notifybot",
    "p_business_id": null,
    "p_about": "주문 상태를 알립니다"
  }'

p_usernamea–z 0–9 _로 4~31자여야 하고 언제나 bot으로 끝나야 합니다(정규식 ^[a-z][a-z0-9_]{2,29}bot$). 봇을 가게의 것으로 두려면 p_business_id를 보내세요 — 호출자는 그 가게의 구성원이어야 하고 bot.manage 권한이 있어야 합니다. 성공 응답은 token을 돌려주는데, 이것이 그 토큰이 평생 한 번 보여 주는 실제 값입니다. 데이터베이스는 sha256 해시만 보관합니다 — 놓치면 새로 발급받아야 하며(bot_rotate_token) 실제 값을 다시 읽을 방법은 없습니다.

{ "ok": true, "bot_id": 12, "user_id": 4821, "username": "notifybot", "token": "kbot_12_…" }

2. 봇을 채팅방에 초대하기 — bot_invite

실제로 그 방에 있는 사람이어야 하고(그 방이 가게 메시지함이라면 가게 직원이어야 합니다), 이 봇을 관리할 수 있어야 합니다(can_manage_bot). p_targetchats.id입니다.

curl -X POST "https://<SUPABASE_PROJECT>.supabase.co/rest/v1/rpc/bot_invite" \
  -H "apikey: <SUPABASE_ANON_KEY>" \
  -H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{ "p_bot": 12, "p_kind": "chat", "p_target": 9931 }'

2-1. 봇을 그룹 방 / 직원 방에 초대하기 — 채팅방보다 권한이 엄격합니다

p_kind: "community_room" 또는 "business_room"p_target(각각 community_rooms.id/business_rooms.id)과 함께 보내는 것은 이미 실제로 됩니다 — 다만 초대하는 사람은 그 방의 의미에 맞는 권한 관문을 통과해야 하며, 단순히 구성원인 것만으로는 되지 않습니다.

  • 그룹 방 — 초대하는 사람이 그 방의 규칙(read_role/post_role)에 따라 실제로 그 방에 글을 쓸 수 있어야 합니다. 보통의 대화방은 구성원이 초대할 수 있고, 관리자 전용으로 둔 공지방·백오피스 방은 그룹의 소유자나 관리자만 할 수 있습니다
  • 가게 직원 방 — 기존 방의 관문(읽기 권한/글쓰기 권한/수신자 규칙)을 모두 통과하고 여기에 더해 언제나 teamroom.manage capability가 있어야 합니다 — 회사 내부 방에 봇을 데려오는 일은 "방에 글쓰기"가 아니라 "방 관리"에 해당하기 때문입니다

두 경우 모두 언제나 그 봇의 관리자여야 합니다(can_manage_bot) — 공용 방은 아무나 남의 봇을 끌고 들어와 말하게 하는 곳이 아닙니다.

🔴 invite_stale — 쓰기 전에 알아야 할 가장 중요한 것

초대한 사람의 권한은 초대할 때만 검사되지 않습니다bot_send는 메시지를 보낼 때마다 같은 초대자의 이름으로 방의 규칙을 다시 묻습니다 ⇒ 초대한 사람이 그룹에서 나가거나 / 역할이 내려가거나 / teamroom.manage를 빼앗기거나 / 방이 닫히는 순간, 봇의 다음 메시지는 곧바로 reason: "invite_stale"을 받습니다 — 아무도 쫓아내지 않았는데 봇이 스스로 조용해집니다. 배경 작업도, 빠짐없이 쏘아야 하는 트리거도 없습니다. 순전히 권한을 다시 계산한 결과입니다

메시지 보내기 — POST /api/bot/send

토큰은 두 가지 방식으로 보낼 수 있고 하나만 고르면 됩니다.

# 방식 1 — Authorization: Bearer
curl -X POST "https://koratland.com/api/bot/send" \
  -H "Authorization: Bearer kbot_12_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-9931-shipped" \
  -d '{ "kind": "chat", "chat_id": 9931, "text": "물건이 가게에서 출발했습니다" }'
# 방식 2 — X-Bot-Token
curl -X POST "https://koratland.com/api/bot/send" \
  -H "X-Bot-Token: kbot_12_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "kind": "chat", "chat_id": 9931, "text": "물건이 가게에서 출발했습니다" }'

토큰을 아예 보내지 않으면 401이 돌아옵니다.

{ "ok": false, "reason": "bad_token", "detail": "토큰은 Authorization: Bearer … 또는 X-Bot-Token으로만 보내세요" }

본문의 필드

필드필수
kindstring아니요 (기본값 "chat")목적지의 종류를 정합니다 — 실제로 쓸 수 있는 값은 셋입니다: "chat" · "community_room" · "business_room". 다른 값은 데이터베이스가 곧바로 reason: "bad_target"을 돌려줍니다(조용히 "chat"으로 넘어가는 fallback은 없습니다 — 이 값을 고정해 두던 예전 버그는 고쳐졌습니다)
chat_idinteger > 0예 (또는 target_id)봇이 이미 초대된 목적지 방의 id — kind: "chat"이면 chats.id, "community_room"이면 community_rooms.id, "business_room"이면 business_rooms.id입니다(본문의 필드 이름은 언제나 chat_id/target_id이며 kind마다 이름이 따로 있지 않습니다)
textstring메시지입니다. 비울 수 없고 text_max_message 레지스트리가 정한 상한을 넘을 수 없습니다(상한은 고정된 숫자가 아니라 조정할 수 있습니다 — too_long 오류의 max 값을 보세요)
client_keystring아니요중복 메시지를 막는 키이며 Idempotency-Key 헤더 대신 쓸 수 있습니다 — 8~128자이고 A–Z a–z 0–9 _ . : -만 쓸 수 있습니다

1:1 채팅과 가게 채팅 — 어느 필드가 가르나

kind는 목적지 테이블의 종류(채팅방 / 그룹 방 / 가게 직원 방)를 가릅니다. 반면 1:1 채팅과 가게 메시지함kind로 나뉘지 않습니다 — 둘 다 똑같이 chats 테이블의 행이며, chats.business_id에 값이 있는지 없는지로 갈립니다(값이 있으면 가게 메시지함). 초대 권한 검사, 사용자 차단 검사, 그리고 "가게는 손님에게 먼저 말을 걸 수 없다(conversation_not_open)"는 규칙도 모두 이 칼럼에서 읽습니다 — 보내는 chat_id는 이미 올바른 방이어야 하며, API에는 "개인/사업" 을 고르는 파라미터가 따로 없습니다.

Idempotency-Key와 발송 한도

표준 Idempotency-Key 헤더나 본문의 client_key 필드를 보내세요 — 하나만 고르면 됩니다. 둘 다 보낸다면 값이 같아야 하며, 다르면 422 idempotency_key_conflict가 돌아옵니다. 같은 키(같은 방 + 같은 키)로 같은 요청을 다시 쏘면 "duplicate": true가 붙은 예전 200 응답이 돌아오고 메시지가 또 만들어지지 않습니다.

한도는 두 겹입니다: IP 단위(Worker 자체에서 분당 120 요청)와 봇 단위(봇 하나당 기본 분당 20건, 하루 1000건이며 관리자가 봇마다 조정할 수 있습니다). 둘 다 429와 함께 Retry-After 헤더를 초 단위로 돌려줍니다(IP 단위 = 언제나 60 · 봇 분당 = 60 · 봇 하루 = 3600).

메시지 받기 — 웹훅

봇이 초대된 채팅방에 누군가 메시지를 쓰면(그리고 보낸 이가 봇 자신의 계정이 아니라면 — 봇의 메시지가 자기에게 되돌아오는 것을 막습니다) Korat이 정해 둔 URL로 POST를 쏩니다. 토큰을 받을 때와 마찬가지로, 로그인한 사용자 계정으로 그 봇을 관리할 권한(can_manage_bot)을 가지고 RPC를 통해 설정해야 합니다.

curl -X POST "https://<SUPABASE_PROJECT>.supabase.co/rest/v1/rpc/bot_set_webhook" \
  -H "apikey: <SUPABASE_ANON_KEY>" \
  -H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{ "p_bot": 12, "p_url": "https://your-server.example.com/korat-webhook" }'
{ "ok": true, "secret": "a1b2c3…", "note": "이 secret은 한 번만 보입니다 — 서명을 직접 검증하려면 보관하세요" }

URL은 반드시 https://여야 하며, 내부·루프백·링크로컬 호스트(localhost, 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 모든 클라우드의 메타데이터 엔드포인트 포함)여서는 안 됩니다 — 설정하는 순간 거절하고, 나중에 DNS가 다른 곳을 가리키는 것을 막으려고 쏘기 전에 매번 실제 DNS를 resolve해 다시 확인합니다. secret은 설정하거나 새로 돌릴 때 한 번만 보입니다 — bot_rotate_webhook_secret(p_bot)으로 언제든 새로 돌리고, bot_disable_webhook(p_bot)/bot_enable_webhook(p_bot)으로 직접 끄고 켜며, bot_webhook_status(p_bot)으로 상태를 봅니다(secret은 돌려주지 않습니다).

오늘은 실제로 아무것도 나가지 않습니다

발송 큐(bot_webhook_deliveries)는 바르게 동작하고 데이터베이스에서 증명되었으며, 발송 경로의 secret(BOT_WEBHOOK_DISPATCH_SECRET)도 소유자가 설정했습니다. 그러나 큐를 끌어다 실제로 쏘는 것(매분 Worker를 불러야 하는 pg_cron)이 아직 프로덕션에 설정되지 않았습니다 — 웹훅을 설정해 둘 수 있고 메시지는 큐에 쌓여 기다리지만, pg_cron 시계가 설정되기 전까지 여러분의 엔드포인트는 아무것도 받지 못합니다

여러분의 엔드포인트가 받게 될 요청의 모습

POST /korat-webhook HTTP/1.1
Content-Type: application/json
X-Korat-Signature: sha256=<hex>
X-Korat-Timestamp: 1750000000000

{
  "event": "message.created",
  "chat_id": 9931,
  "message_id": 88213,
  "text": "빨간색 있나요",
  "time": "14:02",
  "sender": { "id": 4821, "username": "somchai_p", "name": "솜차이 P." }
}

이것이 전부입니다 — 본문에 누구의 전화번호도, 이메일도, 토큰도 없습니다. 봇은 답하는 데 필요한 만큼의 방·메시지·보낸 이의 신원만 봅니다.

요청을 믿기 전에 서명을 검증하세요

X-Korat-SignatureHMAC-SHA256(secret, "<timestamp>." + rawBody)를 16진수로 인코딩한 값입니다 — Node.js 예제입니다.

const crypto = require("crypto");
function verify(rawBody, signatureHeader, timestampHeader, secret) {
  const age = Date.now() - Number(timestampHeader);
  if (!(age >= 0 && age < 5 * 60_000)) return false; // 재전송 방지: 5분을 넘기면 안 됩니다
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(`${timestampHeader}.${rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}

8초 안에 2xx로 답하면 발송에 성공한 것으로 봅니다 — 그 밖의 응답(Korat이 따라가지 않는 3xx 리다이렉트 포함)이나 아무 응답이 없으면 실패로 보고 재시도 큐에 넣습니다.

엔드포인트가 죽었을 때의 재시도

실패하면 지수적으로 물러납니다(1, 2, 4, 8, 16, 32, 64, 128분). 메시지 하나당 최대 8번까지 하고 그 메시지는 포기합니다. 엔드포인트가 연달아 15번 실패하면(여러 메시지에 걸쳐) 웹훅이 자동으로 꺼지고 봇 소유자에게 앱으로 알림이 갑니다 — 죽은 엔드포인트에 영원히 쏘는 일은 결코 없습니다. 엔드포인트 문제를 고친 뒤 bot_enable_webhook으로 다시 켤 수 있습니다.

전체 오류 코드 표

실패했을 때의 모든 응답에는 언제나 {"ok": false, "reason": "…"}가 있습니다 — 판단에 써야 하는 것은 HTTP status가 아니라 reason입니다.

HTTPreason어디서뜻 / 해결
401bad_tokenWorker / DB토큰을 보내지 않았거나, 토큰이 틀렸거나 폐기됐거나 봇이 꺼졌습니다 — 추측을 막으려고 일부러 모두 같은 말로 답합니다
403not_invitedDB봇이 이 방에 초대된 적이 없거나 쫓겨났습니다 — 먼저 bot_invite를 부르세요
403blockedDB방의 사용자가 이 봇을 차단해 두었습니다(가게 메시지함이 아닌 방에 한합니다)
403account_suspendedDB방의 어느 한쪽 계정이 정지되었습니다 — 잘못된 요청이 아니라 권한의 문제입니다
404no_such_chatDBchat_id가 실제로 없습니다
404no_such_endpointWorker경로가 틀렸습니다. POST /api/bot/send만 있습니다
405method_not_allowedWorkerPOST가 아닌 다른 메서드를 썼습니다
409conversation_not_openDB이 방은 가게 메시지함이고 손님이 아직 말을 건 적이 없습니다 — 가게 쪽(봇 포함)은 먼저 말을 걸 수 없습니다
409chat_closedDB매칭(데이팅) 방이 메시지를 더 받지 않습니다
422empty_textDB공백을 걷어내면 text가 비어 있습니다
422too_longDB메시지가 상한을 넘었습니다 — 함께 오는 max 값을 보세요
422bad_targetDBkind"chat"/"community_room"/"business_room" 중 하나가 아닙니다
404no_such_roomDBchat_id에 해당하는 그룹 방·직원 방이 실제로 없습니다(이미 지워진 방입니다)
409room_archivedDB이 방은 닫혔습니다 — 사람도 봇도 쓸 수 없습니다
403invite_staleDB초대한 사람의 권한을 다시 계산했더니 통과하지 못했습니다 — 초대자가 그룹에서 나갔거나, 역할이 내려갔거나, teamroom.manage를 빼앗겼거나, 방이 닫혔습니다. 더 자세한 이유는 detail을 보세요(not_a_member/room_role/not_teamroom_manager) — 기다리지 말고, 아직 권한이 있는 사람이 그 방에 봇을 다시 초대(bot_invite)해야 풀립니다
422bad_jsonWorker요청 본문이 올바른 JSON이 아닙니다
422bad_chat_idWorkerchat_id/target_id는 양의 정수여야 합니다
422bad_textWorkertext는 문자열이어야 합니다
422idempotency_key_conflictWorkerIdempotency-Keyclient_key를 함께 보냈는데 값이 다릅니다 — 하나만 보내면 됩니다
422bad_client_keyWorker중복 방지 키가 형식에 맞지 않습니다(8~128자, A–Z a–z 0–9 _ . : -)
429rate_limitedWorker / DB한도를 넘었습니다 — Retry-After 헤더와 window/retry_after_sec 필드를 확인하세요
502database_errorWorker데이터베이스가 답했지만 2xx가 아닙니다 — 함께 오는 pg_code/detail을 보세요. Postgres 자신의 메시지입니다
503service_key_not_configuredWorker서버에 service key가 아직 설정되지 않았습니다 — 호출자가 아니라 Korat 쪽 문제입니다
504database_unreachableWorker데이터베이스 호출에 실패했거나 시간이 초과됐습니다 — safe_to_retry를 보세요. client_key/Idempotency-Key를 함께 보낸 경우에만 안전하게 다시 쏠 수 있습니다

성공했을 때의 응답

{ "ok": true, "duplicate": false, "message_id": 88213 }

duplicate: true는 이 요청이 앞서 이미 보내졌다는 뜻입니다(client_key/Idempotency-Key로 중복을 막았습니다) — 새 메시지는 만들어지지 않았지만 여전히 성공(200)으로 봅니다. 호출자가 바라던 결과는 이미 실제로 일어났기 때문입니다.