บอทคืออะไร ใช้ทำอะไรได้
บอทคือ "บัญชีผู้ใช้" อีกแบบหนึ่งในระบบของ Korat ที่ไม่มีคนนั่งพิมพ์อยู่หลังจอ — โปรแกรมของคุณเองยิงคำขอเข้ามา แล้วข้อความจะไปโผล่ในห้องแชทที่บอทถูกเชิญไว้ ใช้ได้จริงเช่น:
- แจ้งเตือนออเดอร์ใหม่เข้าแชท — ระบบขายของร้านคุณมีออเดอร์ใหม่ ⇒ ให้บอทพิมพ์แจ้งเข้าห้องแชทกับลูกค้าหรือห้องทีมงานทันที
- แจ้งของใกล้หมด — สต๊อกเหลือน้อยกว่าที่ตั้งไว้ ⇒ บอทพิมพ์เตือนเข้าห้องทีมงานของร้าน
- แจ้งสถานะเซิร์ฟเวอร์/ระบบ — สคริปต์ตรวจสุขภาพระบบของคุณเองรันเสร็จ ⇒ ให้บอทรายงานผลเข้าห้องกลุ่มที่ทีมไอทีดูอยู่
บอททำอะไรไม่ได้บ้าง (ข้อมูลจริงจากฐานข้อมูล ไม่ใช่ข้อจำกัดชั่วคราว):
- บอทอ่านข้อความในห้องไม่ได้ — เป็นขาออกอย่างเดียว ยกเว้นถ้าคุณตั้งเว็บฮุครับข้อความเข้าไว้เอง (ดูหัวข้ออ้างอิงเทคนิคท้ายหน้า) และแม้ตั้งเว็บฮุคไว้ วันนี้ยังไม่มีอะไรถูกส่งจริง เพราะนาฬิกาที่ต้องดึงคิวไปยิงยังไม่ได้ตั้งค่าบนโปรดักชัน
- บอทเชิญตัวเองเข้าห้องไม่ได้ — ต้องมีคนที่อยู่ในห้องนั้นแล้ว (หรือมีสิทธิ์จัดการห้องนั้น) เป็นคนเชิญเสมอ
- บอทหนึ่งตัวถือโทเคนที่ใช้ได้พร้อมกันได้ไม่เกิน 2 ใบ และหนึ่งบัญชีสร้างบอทได้สูงสุด 5 ตัว
เริ่มยังไง — 4 ขั้น
- 1สร้างบอท ตั้งชื่อและชื่อผู้ใช้ให้บอท (ชื่อผู้ใช้ต้องลงท้ายด้วยคำว่า
botเช่นorderbot) — ถ้าอยากให้บอทเป็นของร้าน (ไม่ใช่ของคุณคนเดียว) เลือกร้านตอนสร้างได้เลย - 2คัดลอกโทเคน ระบบจะโชว์โทเคนให้ครั้งเดียวตอนสร้างเสร็จ — คัดลอกเก็บไว้ในที่ปลอดภัยทันที ปิดหน้าจอไปแล้วจะไม่มีทางเรียกค่าจริงกลับมาดูอีก (ต้องออกใบใหม่แทน)
- 3เชิญบอทเข้าห้อง เลือกห้องแชท ห้องกลุ่ม หรือห้องทีมงานที่อยากให้บอทพูด — คุณต้องเป็นคนที่อยู่ในห้องนั้น (หรือมีสิทธิ์จัดการห้องนั้น) อยู่แล้วถึงจะเชิญได้
- 4ยิงข้อความแรก เอาโทเคนจากขั้น 2 ไปเรียก Bot API — ดูตัวอย่างที่ทำตามได้จริงในหัวข้อถัดไป
ขั้น 1–3 ทำในแอป Korat
เมนูอยู่ที่ ตั้งค่า › บัญชี › "บอทของฉัน" — ในนั้นสร้างบอท ออก/หมุน/เพิกถอนโทเคน และเชิญบอทเข้าห้องแชท/ห้องกลุ่ม/ห้องทีมงานได้ทั้งหมดโดยไม่ต้องเขียนโค้ดสักบรรทัด เมนูนี้จะมาพร้อมแอป Korat รุ่นถัดไป — รุ่นที่ดาวน์โหลดใช้งานอยู่วันนี้ยังไม่มี ถ้าเปิดตั้งค่าแล้วยังไม่เห็น "บอทของฉัน" ให้รออัปเดตแอป ระหว่างนี้ทำขั้น 1–3 ผ่าน RPC ของ Supabase ตรง ๆ ได้เหมือนกัน (ดูหัวข้ออ้างอิงเทคนิคท้ายหน้า)
🔒 โทเคนคือรหัสผ่านของบอท
ใครก็ตามที่ถือโทเคนใบนี้ ส่งข้อความในนามบอทได้ทันที — ห้ามส่งโทเคนผ่านแชท รูปภาพ หรืออีเมลที่ไม่เข้ารหัส เก็บไว้ในเซิร์ฟเวอร์/ที่เก็บความลับของคุณเท่านั้น ไม่มีเจ้าหน้าที่ 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 ได้ฟรีด้วยบัญชี Google เดียว — เข้า 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 ที่บันทึกออเดอร์ ใช้ตัวจับเวลา (Triggers) ของ Apps Script ตั้งให้เรียกฟังก์ชันนี้เมื่อชีตถูกแก้ไข (onEdit) หรือทุกกี่นาทีก็ได้
วิธีเดียวกันนี้ใช้ได้กับเครื่องมือ automation ที่มี action "HTTP request"/"Webhook" อยู่แล้ว เช่น n8n, Make หรือ Zapier — ตั้งค่าเป็น POST ไปที่ URL เดียวกัน ใส่หัวคำขอ 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 ตรง ๆ (ไม่ผ่านแอป)
ถ้าแอปรุ่นที่คุณใช้ยังไม่มีจอ "บอทของฉัน" หรืออยากทำแบบอัตโนมัติ เรียก RPC ของ Supabase ตรง ๆ ด้วย Access Token ของผู้ใช้ที่ล็อกอินอยู่ (ไม่ใช่โทเคนบอท) ผ่าน PostgREST — สามขั้นตอน:
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_username ต้องเป็น a–z 0–9 _ ยาว 4–31 ตัว และลงท้ายด้วย bot เสมอ (regex ^[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_target คือ chats.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ก. เชิญบอทเข้าห้องกลุ่ม / ห้องทีมงาน — สิทธิ์เข้มกว่าห้องแชท
ส่ง p_kind: "community_room" หรือ "business_room" พร้อม p_target เป็น community_rooms.id/business_rooms.id ได้จริงแล้ว — แต่ผู้เชิญต้องผ่านด่านสิทธิ์ที่ตรงความหมายของห้องนั้น ไม่ใช่แค่เป็นสมาชิก:
- ห้องกลุ่ม — ผู้เชิญต้องพิมพ์ในห้องนั้นได้จริงตามกติกาของห้องเอง (
read_role/post_role) ห้องคุยธรรมดาสมาชิกเชิญได้ ห้องประกาศ/ห้องหลังบ้านที่ตั้งไว้เฉพาะแอดมินต้องเป็นเจ้าของ/แอดมินกลุ่มเท่านั้น - ห้องทีมงานร้าน — ต้องผ่านด่านของห้องเดิมครบ (สิทธิ์อ่าน/สิทธิ์โพสต์/กฎผู้รับ) บวก capability
teamroom.manageเสมอ — พาบอทเข้าห้องภายในของบริษัทถือเป็น "จัดการห้อง" ไม่ใช่แค่ "พิมพ์ในห้อง"
ทั้งสองแบบยังต้องเป็นผู้จัดการบอทตัวนั้น (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 เท่านั้น" }
ฟิลด์ในบอดี้
| ฟิลด์ | ชนิด | บังคับ | ความหมาย |
|---|---|---|---|
kind | string | ไม่ (ดีฟอลต์ "chat") | ตัวตัดสินชนิดปลายทาง — มีสามค่าที่ใช้ได้จริง: "chat" · "community_room" · "business_room" ค่าอื่นได้ reason: "bad_target" จากฐานข้อมูลตรง ๆ (ไม่มี fallback เงียบ ๆ ไปเป็น "chat" — บั๊กเดิมที่เคยตรึงค่านี้ไว้ถูกแก้แล้ว) |
chat_id | integer > 0 | ใช่ (หรือ target_id) | id ของห้องปลายทางที่บอทถูกเชิญแล้ว — chats.id เมื่อ kind: "chat", community_rooms.id เมื่อ "community_room", business_rooms.id เมื่อ "business_room" (ชื่อฟิลด์ในบอดี้ยังคงเป็น chat_id/target_id เสมอ ไม่มีชื่อแยกตาม kind) |
text | string | ใช่ | ข้อความ ห้ามว่าง ยาวไม่เกินเพดานที่ทะเบียน text_max_message กำหนด (เพดานปรับได้ ไม่ใช่ตัวเลขตายตัว — ดูค่า max ใน error too_long) |
client_key | string | ไม่ | กุญแจกันข้อความซ้ำ ทางเลือกแทนหัวคำขอ Idempotency-Key — ยาว 8–128 ตัว ใช้ได้เฉพาะ A–Z a–z 0–9 _ . : - |
แชทส่วนตัว vs แชทกลุ่มธุรกิจ — ฟิลด์ไหนตัดสิน
kind เป็นตัวตัดสินชนิดของตารางปลายทาง (ห้องแชท / ห้องกลุ่ม / ห้องทีมงานร้าน) ส่วนแชทส่วนตัวกับกล่องข้อความของร้านไม่ได้แยกด้วย kind — ทั้งคู่เป็นแถวในตาราง chats เหมือนกัน ต่างกันที่ chats.business_id มีค่าหรือไม่ (มีค่า = กล่องข้อความของร้าน) การตรวจสิทธิ์เชิญ การตรวจบล็อกผู้ใช้ และกติกา "ร้านทักลูกค้าก่อนไม่ได้ (conversation_not_open)" ก็อ่านจากคอลัมน์นี้เช่นกัน — chat_id ที่ส่งมาต้องเป็นห้องที่ถูกต้องอยู่แล้ว ตัว API ไม่มีพารามิเตอร์แยก "ส่วนตัว/ธุรกิจ" ให้เลือก
Idempotency-Key และเพดานอัตรา
ส่งหัวคำขอมาตรฐาน Idempotency-Key หรือฟิลด์ client_key ในบอดี้ — เลือกอย่างใดอย่างหนึ่ง ถ้าส่งมาทั้งคู่ค่าต้องตรงกัน ไม่ตรงกันได้ 422 idempotency_key_conflict ยิงคำขอเดิมซ้ำด้วยคีย์เดิม (ห้อง+คีย์เดียวกัน) จะได้คำตอบ 200 เดิมพร้อม "duplicate": true ไม่สร้างข้อความซ้ำ
เพดานมีสองชั้น: ต่อ IP (120 คำขอ/นาที ที่ตัว Worker เอง) กับ ต่อบอท (ค่าเริ่มต้น 20/นาที และ 1000/วัน ต่อบอทหนึ่งตัว ปรับได้รายตัวโดยแอดมิน) ทั้งสองแบบตอบ 429 พร้อมหัว Retry-After เป็นวินาที (ต่อ IP = 60 เสมอ · ต่อบอทต่อนาที = 60 · ต่อบอทต่อวัน = 3600)
รับข้อความเข้า — เว็บฮุค
เมื่อมีคนพิมพ์ข้อความเข้าห้องแชทที่บอทถูกเชิญไว้แล้ว (และผู้ส่งไม่ใช่บัญชีของบอทเอง — กันข้อความของบอทสะท้อนกลับหาตัวเอง) Korat จะยิง POST ไปที่ URL ที่ตั้งไว้ ต้องตั้งค่าผ่าน RPC ด้วยบัญชีผู้ใช้ที่ล็อกอินอยู่และมีสิทธิ์จัดการบอทตัวนั้น (can_manage_bot) เหมือนขั้นตอนขอโทเคน:
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:// เท่านั้น และห้ามเป็นโฮสต์ภายใน/loopback/link-local (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 รวม endpoint ข้อมูลเมทาของคลาวด์ทุกเจ้า) — ปฏิเสธตั้งแต่ตอนตั้งค่า และตรวจซ้ำอีกครั้งด้วยการ resolve DNS จริงทุกครั้งก่อนยิง กัน DNS ชี้ไปที่อื่นภายหลัง 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) เจ้าของตั้งแล้ว แต่ตัวที่ดึงคิวไปยิงจริง (pg_cron ที่ต้องเรียก Worker ทุกนาที) ยังไม่ได้ตั้งค่าบนโปรดักชัน — ตั้งเว็บฮุคไว้ได้ ข้อความจะเข้าคิวรอ แต่ปลายทางของคุณจะไม่มีวันได้รับอะไรจนกว่านาฬิกา 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": "สมชาย พ." }
}
เท่านี้เอง — ไม่มีเบอร์โทร อีเมล หรือโทเคนของใครทั้งสิ้น ในบอดี้ บอทเห็นแค่ห้อง/ข้อความ/ตัวตนของผู้ส่งเท่าที่จำเป็นต่อการตอบ
ตรวจลายเซ็นก่อนเชื่อคำขอ
X-Korat-Signature คือ HMAC-SHA256(secret, "<timestamp>." + rawBody) เข้ารหัสเป็นเลขฐานสิบหก — ตัวอย่าง 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; // กัน replay: อายุต้องไม่เกิน 5 นาที
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(`${timestampHeader}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
ตอบ 2xx ภายใน 8 วินาทีถือว่าส่งสำเร็จ — ตอบอย่างอื่น (รวม redirect 3xx ซึ่ง Korat ไม่ตามให้) หรือไม่ตอบเลยถือว่าล้มเหลวและเข้าคิวลองใหม่
ลองใหม่เมื่อปลายทางล่ม
ล้มเหลว ⇒ ถอยแบบทวีคูณ (1, 2, 4, 8, 16, 32, 64, 128 นาที) สูงสุด 8 ครั้งต่อข้อความ แล้วเลิกส่งข้อความนั้น ถ้าปลายทางล้มเหลวติดกัน 15 ครั้ง (ข้ามหลายข้อความ) เว็บฮุคจะถูกปิดเองอัตโนมัติ พร้อมแจ้งเตือนเจ้าของบอทในแอป — ไม่มีวันยิงตลอดกาลใส่ปลายทางที่ตายแล้ว เรียก bot_enable_webhook เปิดใหม่ได้หลังแก้ปัญหาที่ปลายทาง
ตารางรหัสผิดพลาดทั้งหมด
ทุกคำตอบมี {"ok": false, "reason": "…"} เสมอเมื่อไม่สำเร็จ — reason คือคำที่ต้องใช้ตัดสินใจ ไม่ใช่แค่ HTTP status
| HTTP | reason | มาจาก | ความหมาย / วิธีแก้ |
|---|---|---|---|
| 401 | bad_token | Worker / DB | ไม่ได้ส่งโทเคน หรือโทเคนผิด/ถูกเพิกถอนแล้ว/บอทถูกปิด — ตั้งใจตอบคำเดียวกันหมดเพื่อกันไล่เดา |
| 403 | not_invited | DB | บอทยังไม่เคยถูกเชิญเข้าห้องนี้ หรือถูกเตะออกแล้ว — เรียก bot_invite ก่อน |
| 403 | blocked | DB | ผู้ใช้ในห้องบล็อกบอทตัวนี้ไว้ (เฉพาะห้องที่ไม่ใช่กล่องข้อความร้าน) |
| 403 | account_suspended | DB | ฝั่งใดฝั่งหนึ่งในห้องถูกระงับบัญชี — เป็นเรื่องสิทธิ์ ไม่ใช่คำขอผิด |
| 404 | no_such_chat | DB | chat_id ไม่มีอยู่จริง |
| 404 | no_such_endpoint | Worker | เส้นทางผิด มีแค่ POST /api/bot/send |
| 405 | method_not_allowed | Worker | ใช้เมธอดอื่นที่ไม่ใช่ POST |
| 409 | conversation_not_open | DB | ห้องนี้เป็นกล่องข้อความของร้านและลูกค้ายังไม่เคยทักมาก่อน — ฝั่งร้าน (รวมบอท) ทักก่อนไม่ได้ |
| 409 | chat_closed | DB | ห้องจับคู่ (dating) ปิดรับข้อความแล้ว |
| 422 | empty_text | DB | text ว่างเปล่าหลังตัดช่องว่าง |
| 422 | too_long | DB | ข้อความยาวเกินเพดาน — ดูค่า max ที่แนบมาด้วย |
| 422 | bad_target | DB | kind ไม่ใช่หนึ่งใน "chat"/"community_room"/"business_room" |
| 404 | no_such_room | DB | chat_id ไม่มีห้องกลุ่ม/ห้องทีมงานอยู่จริง (ห้องถูกลบไปแล้ว) |
| 409 | room_archived | DB | ห้องนี้ถูกปิดแล้ว — พิมพ์ไม่ได้ทั้งคนและบอท |
| 403 | invite_stale | DB | สิทธิ์ของผู้เชิญถูกคำนวณใหม่และไม่ผ่านแล้ว — ผู้เชิญออกจากกลุ่ม/ถูกลดบทบาท/ถูกถอด teamroom.manage/ห้องถูกปิด ดู detail สำหรับเหตุผลย่อย (not_a_member/room_role/not_teamroom_manager) — แก้โดยให้คนที่ยังมีสิทธิ์เชิญบอทเข้าห้องนั้นใหม่ (bot_invite) ไม่ใช่แค่รอ |
| 422 | bad_json | Worker | บอดี้คำขอไม่ใช่ JSON ที่ถูกต้อง |
| 422 | bad_chat_id | Worker | chat_id/target_id ต้องเป็นจำนวนเต็มบวก |
| 422 | bad_text | Worker | text ต้องเป็นสตริง |
| 422 | idempotency_key_conflict | Worker | ส่ง Idempotency-Key กับ client_key มาพร้อมกันแต่ค่าไม่ตรงกัน — ส่งมาอย่างเดียวพอ |
| 422 | bad_client_key | Worker | คีย์กันซ้ำไม่ตรงรูปแบบ (8–128 ตัว, A–Z a–z 0–9 _ . : -) |
| 429 | rate_limited | Worker / DB | เกินเพดาน — เช็คหัว Retry-After และฟิลด์ window/retry_after_sec |
| 502 | database_error | Worker | ฐานข้อมูลตอบกลับมาแต่ไม่ใช่ 2xx — ดู pg_code/detail ที่แนบมา คือข้อความจริงของ Postgres |
| 503 | service_key_not_configured | Worker | เซิร์ฟเวอร์ยังไม่ตั้งค่า service key — ปัญหาฝั่ง Korat ไม่ใช่ฝั่งผู้เรียก |
| 504 | database_unreachable | Worker | เรียกฐานข้อมูลไม่สำเร็จหรือหมดเวลา — ดู safe_to_retry: ยิงซ้ำได้อย่างปลอดภัยก็ต่อเมื่อส่ง client_key/Idempotency-Key มาด้วย |
คำตอบเมื่อสำเร็จ
{ "ok": true, "duplicate": false, "message_id": 88213 }
duplicate: true แปลว่าคำขอนี้ถูกส่งไปแล้วก่อนหน้า (กันซ้ำด้วย client_key/Idempotency-Key) — ไม่มีข้อความใหม่ถูกสร้าง แต่ยังถือเป็นความสำเร็จ (200) เพราะผลลัพธ์ที่ผู้เรียกต้องการเกิดขึ้นแล้วจริง