เชื่อมแชทบอทเข้ากับแอปมือถือ
https://api.agenticos.tech/api/v1/omnichat1. ภาพรวม
ทุก endpoint ของแอปเป็น public ยืนยันสิทธิ์ด้วย embed token อย่างเดียว ไม่ต้องมี JWT มีค่าที่ต้องใช้ 3 ตัว แยกหน้าที่กัน:
| ค่า | ได้จากไหน | เก็บไว้ที่ |
|---|---|---|
| Embed token | OmniChat → ช่องทาง → ช่องทางเว็บ → โค้ดฝัง (ค่า data-omnichat-token) | ในแอปได้ (ค่าสาธารณะ) |
| Identity key | หน้าเดียวกัน → "เชื่อมกับระบบสมาชิกของเว็บคุณ" (ไม่บังคับ) | เซิร์ฟเวอร์ของแอปเท่านั้น |
| Webhook signing key | หน้าเดียวกัน → "แจ้งเตือนในแอปมือถือเมื่อมีคำตอบ" (ไม่บังคับ) | เซิร์ฟเวอร์ของแอปเท่านั้น |
ก่อนเริ่ม: ต้องขออะไรจากแพลตฟอร์ม
ค่าและการตั้งค่าด้านล่างอยู่ในบัญชี AgenticOS ขององค์กรลูกค้า คนที่ให้ได้คือแอดมินขององค์กร (owner หรือ admin) ขอให้ครบก่อนเริ่มเขียนโค้ด
| ต้องขอ | ใช้ทำอะไร | แอดมินหาได้ที่ | จำเป็น |
|---|---|---|---|
| ช่องทางเว็บที่ตั้งบอทแล้ว | ให้มีบอทตอบ: ช่องทางต้องผูกกับทีม (desk) ที่มี AI agent เปิดตอบอัตโนมัติ และ agent ผูกคลังความรู้แล้ว | OmniChat → ช่องทาง → เพิ่มช่องทาง → เว็บไซต์ | จำเป็น |
| Embed token | ใส่ในทุกคำขอจากแอป | ช่องทางเว็บ → โค้ดฝัง → ค่า data-omnichat-token | จำเป็น |
| Identity key | ผูกแชทกับบัญชีสมาชิกในแอป (ข้อ 2.2) | ช่องทางเว็บ → เชื่อมกับระบบสมาชิกของเว็บคุณ → สร้างกุญแจ | ถ้าแอปมีระบบสมาชิก |
| Webhook signing key | ตรวจลายเซ็น webhook สำหรับ push (ข้อ 4) | ช่องทางเว็บ → แจ้งเตือนในแอปมือถือเมื่อมีคำตอบ → ใส่ URL ที่ dev ส่งให้ แล้วคัดลอกกุญแจ | ถ้าต้องการ push |
| เพดานข้อความต่อนาที | ค่าเริ่มต้น 20 ต่อ token ต่อ IP ถ้าผู้ใช้หลายคนออกเน็ตผ่าน IP เดียวกัน (เช่น Wi-Fi สำนักงาน) อาจต้องเพิ่ม | ตั้งค่า OmniChat → ป้องกันแชทหน้าเว็บ → จำกัดอัตราข้อความ | ถ้าจำเป็น |
| ฟอร์มก่อนแชท เปิดหรือปิด | ถ้าเปิด แอปต้องทำหน้าฟอร์ม (ข้อ 3.5) | ช่องทางเว็บ → บังคับกรอกข้อมูลติดต่อก่อนแชท | ต้องรู้ |
สวัสดีครับ ทีมพัฒนาแอปจะเชื่อมแชทบอท AgenticOS เข้ากับแอปมือถือ รบกวนขอข้อมูลดังนี้ครับ
1. Embed token ของช่องทางเว็บที่ตั้งบอทตอบไว้แล้ว (OmniChat → ช่องทาง → เว็บไซต์ → โค้ดฝัง)
2. Identity key (ช่องทางเว็บ → เชื่อมกับระบบสมาชิกของเว็บคุณ → สร้างกุญแจ) — ส่งผ่าน password manager
3. ใส่ Webhook URL นี้: https://<เซิร์ฟเวอร์ของเรา>/omnichat/reply แล้วส่ง signing key กลับมา — ส่งผ่าน password manager
4. ฟอร์มก่อนแชทเปิดอยู่หรือไม่ และขอเพิ่มเพดานข้อความเป็น __ ครั้ง/นาที (ถ้าจำเป็น)ทางเลือก: ใช้ WebView (ไม่ต้องทำหน้าแชทเอง)
ถ้ายังไม่ต้องการหน้าแชท native เปิดหน้าแชทสำเร็จรูปของ AgenticOS ใน WebView ได้เลย ได้ทุกฟีเจอร์ของแชทหน้าเว็บทันที (บอทตอบจากคลังความรู้ ปุ่มคำถามแนะนำ ส่งรูป ฟอร์มก่อนแชท การ์ดสินค้า ลิงก์ชำระเงิน) มี 2 แบบ
| แบบ | เปิดอะไร | ผูกสมาชิก | เหมาะกับ |
|---|---|---|---|
| A · หน้าแชทเต็มจอ | https://app.agenticos.tech/chat/<embed token> | ไม่ได้ ทุกคนเป็นผู้ใช้ไม่ระบุตัวตน | แอปที่ไม่มีระบบสมาชิก หรืออยากลองเร็วที่สุด |
| B · หน้า HTML ของคุณ + วิดเจ็ตแบบ inline | หน้าเล็ก ๆ ที่เซิร์ฟเวอร์ของคุณสร้าง ใส่ user_id และ user_hash ของผู้ใช้ | ได้ แชทตามผู้ใช้ทุกเครื่อง และใช้ push ได้ | แอปที่มีระบบสมาชิก (แนะนำ) |
แบบ B: หน้า HTML ที่เซิร์ฟเวอร์ของคุณส่งให้ WebView
เซิร์ฟเวอร์สร้างหน้านี้ต่อผู้ใช้หนึ่งคน โดยคำนวณ user_hash บนเซิร์ฟเวอร์ (ข้อ 2.2) แล้วแอปเปิด URL ของหน้านี้ใน WebView CSS ในหน้าทำให้แชทเต็มจอ (วิดเจ็ตแบบ inline สูง 480px เป็นค่าเริ่มต้น)
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
html, body { margin: 0; height: 100%; }
#omnichat-chat { height: 100%; }
.ocw-inline { height: 100% !important; border: 0 !important; border-radius: 0 !important; }
</style>
</head>
<body>
<div id="omnichat-chat"></div>
<script src="https://app.agenticos.tech/widget.js"
data-omnichat-token="EMBED_TOKEN"
data-omnichat-inline="omnichat-chat"
data-omnichat-user-id="{{ user.id }}"
data-omnichat-user-hash="{{ user_hash }}"
data-omnichat-user-name="{{ user.name }}"
async></script>
</body>
</html>- ผู้ใช้ออกจากระบบในแอป: เรียก
omnichatEndSession()ในหน้า (เช่น evaluateJavascript) หรือล้างข้อมูล WebView ก่อนให้คนอื่นล็อกอินบนเครื่องเดียวกัน - push ใช้ได้กับแบบ B เท่านั้น เพราะ webhook บอก user_id ให้เซิร์ฟเวอร์หาเครื่องของผู้ใช้ได้ แบบ A ไม่มีตัวตนให้ส่ง push
ตั้งค่า WebView ให้แชททำงานครบ
| เรื่อง | iOS (WKWebView) | Android (WebView) |
|---|---|---|
| JavaScript และ localStorage (เก็บ session ของแชท) | เปิดอยู่แล้ว ใช้ WKWebsiteDataStore.default() เพื่อให้จำ session ข้ามการเปิดแอป | javaScriptEnabled = true · domStorageEnabled = true |
| ส่งรูป/ไฟล์ | รองรับ input file เอง ใส่ NSPhotoLibraryUsageDescription และ NSCameraUsageDescription ใน Info.plist | ต้องทำ onShowFileChooser ใน WebChromeClient |
| ลิงก์ชำระเงิน ไฟล์แนบ และลิงก์ภายนอก (เปิดแบบ _blank) | ทำ createWebViewWith ใน WKUIDelegate แล้วเปิดด้วย UIApplication.shared.open | ทำ shouldOverrideUrlLoading: host ที่ไม่ใช่หน้าแชท ให้เปิดใน browser ภายนอก |
| คีย์บอร์ดบังช่องพิมพ์ | ใช้ safe area ตามปกติ | android:windowSoftInputMode="adjustResize" |
// iOS — open _blank links (payment, attachments) outside the chat
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration,
for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
if let url = navigationAction.request.url { UIApplication.shared.open(url) }
return nil
}// Android
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, req: WebResourceRequest): Boolean {
val host = req.url.host ?: return false
if (host.endsWith("agenticos.tech")) return false // stay in the chat
startActivity(Intent(Intent.ACTION_VIEW, req.url)); return true
}
}
webView.webChromeClient = object : WebChromeClient() {
override fun onShowFileChooser(view: WebView, cb: ValueCallback<Array<Uri>>,
params: FileChooserParams): Boolean {
filePathCallback = cb
fileChooser.launch(params.createIntent()) // registerForActivityResult(StartActivityForResult)
return true
}
}
webView.loadUrl(chatPageUrl)2. ตัวตนของผู้ใช้ในแชท
2.1 แบบไม่ระบุตัวตน
แอปสร้าง session_id ครั้งเดียวต่อการติดตั้ง แล้วเก็บใน Keychain หรือ EncryptedSharedPreferences
- สุ่มด้วยตัวสุ่มที่ปลอดภัย (CSPRNG) เพราะ session_id เป็นสิ่งเดียวที่ปกป้องประวัติแชทแบบไม่ระบุตัวตน
- ยาว 8–128 ตัวอักษร และห้ามขึ้นต้นด้วย
usr:(สงวนไว้ ส่งมาจะได้ 400 reserved_session_id) - รูปแบบที่วิดเจ็ตเว็บใช้
wsx-ตามด้วย hex 32 ตัว เช่นwsx-1ecb99647586f8100cb4b7ca25d51ac9 - ข้อเสีย: ลบแอปหรือเปลี่ยนเครื่อง จะเริ่มบทสนทนาใหม่
2.2 ผูกกับบัญชีสมาชิก (แนะนำ)
ส่ง user_id และ user_hash ไปด้วย บทสนทนาจะผูกกับสมาชิก ตามไปทุกเครื่อง และทีมงานเห็นชื่อจริง
user_hash = hex( HMAC-SHA256( key = identity key, message = user_id ) ) // lowercase hex, 64 chars- คำนวณบนเซิร์ฟเวอร์ของแอปเท่านั้น แอปขอ user_hash จาก backend ของตัวเองหลังล็อกอิน ห้ามฝัง identity key ในแอป
- user_hash ผิด ไม่มี หรือช่องทางยังไม่เปิด identity: ระบบไม่ error แต่ถือเป็นผู้ใช้ไม่ระบุตัวตน และคุยใน thread ของ session_id แทน
- ยังต้องส่ง session_id ทุกครั้งเหมือนเดิม
const userHash = crypto.createHmac("sha256", process.env.OMNICHAT_IDENTITY_KEY)
.update(String(user.id)).digest("hex");user_hash = hmac.new(IDENTITY_KEY.encode(), str(user.id).encode(), hashlib.sha256).hexdigest()$userHash = hash_hmac('sha256', (string)$user->id, OMNICHAT_IDENTITY_KEY);3. Endpoint
ทุก endpoint ใช้ชื่อสำรอง /web/... แทน /webhooks/web/... ได้
GET/webhooks/web/config
ข้อมูลหน้าจอแชท เรียกครั้งเดียวตอนเปิดหน้าแชท ด้วย ?token=<embed token>
{
"name": "Website chat",
"avatarUrl": "https://…/avatar.png",
"style": { "color": "#6366f1", "color2": "#22d3ee", "gradient": true, "title": "…", "placeholder": "…" },
"prechat": null,
"chatUrl": "https://app.agenticos.tech/chat/<token>",
"suggestions": ["What can you do?", "Pricing", "Talk to our team"],
"followup": true
}| key | ใช้ทำอะไร |
|---|---|
name avatarUrl | ชื่อและรูปของบอท (avatarUrl อาจเป็น null) |
suggestions | ปุ่มคำถามแนะนำ สูงสุด 6 ปุ่ม กดแล้วส่งข้อความนั้นตามปกติ |
prechat | ฟอร์มก่อนแชท ถ้าเปิดไว้ (ดู 3.5) ค่า null คือไม่ต้องทำ |
style | สีและข้อความของวิดเจ็ตเว็บ แอปจะใช้หรือไม่ใช้ก็ได้ |
token ผิดจะได้ 200 พร้อม {"name": null, "avatarUrl": null, "style": null} ไม่ใช่ 401
POST/webhooks/web
ส่งข้อความ (JSON)
{
"token": "<embed token>",
"session_id": "wsx-1ecb99647586f8100cb4b7ca25d51ac9",
"text": "Machine 3 at the Lat Phrao branch is not working",
"user_id": "member-42",
"user_hash": "<64 hex>",
"user_name": "Somying Jaidee",
"user_phone": "0812345678",
"user_email": "[email protected]"
}| field | บังคับ | จำกัด |
|---|---|---|
token | ✓ | 8–200 |
session_id | ✓ | 6–128 (แนะนำ 8–128) ห้ามขึ้นต้น usr: |
text | ✓ | 1–4000 ตัวอักษร |
visitor_name | ≤120 ชื่อแสดงของผู้ใช้ไม่ระบุตัวตน | |
user_id user_hash | ≤100, ≤64 ดูข้อ 2.2 | |
user_name user_email user_phone | ≤120, ≤200, ≤40 ใช้เมื่อ user_hash ถูกต้องเท่านั้น |
| สถานะ | body | ความหมาย |
|---|---|---|
| 200 | {"ok": true, "conversation_id": "<uuid>"} | รับแล้ว บอทจะตอบใน 1–10 วินาที ให้ poll |
| 401 | {"ok": false} | token ผิด หรือช่องทางถูกลบ |
| 429 | {"ok": false, "error": "rate_limited"} | ส่งถี่เกิน (ดูข้อ 5) |
| 400 | {"ok": false, "error": "reserved_session_id"} | session_id ขึ้นต้น usr: |
| 409 | {"ok": false, "error": "prechat_required"} | ต้องส่งฟอร์มก่อนแชทก่อน (3.5) |
| 422 | {"detail": [...]} | field ผิดรูปแบบหรือยาวเกิน (error มาตรฐานของ FastAPI) |
GET/webhooks/web/poll
?token=…&session_id=…&after=<cursor>&user_id=…&user_hash=…
{
"messages": [
{
"direction": "out",
"sender_type": "ai",
"text": "Hi! How can I help?",
"created_at": "2026-09-30T08:15:02.418221+00:00",
"attachment_url": null,
"attachment_kind": null,
"attachment_name": null,
"card": null,
"checkout_url": null
}
],
"cursor": "2026-09-30T08:15:02.418221+00:00",
"prechat_done": true
}| key ของข้อความ | ค่า |
|---|---|
direction | "in" ผู้ใช้ส่ง · "out" ฝั่งธุรกิจตอบ |
sender_type | "customer" · "ai" บอท · "human" ทีมงาน |
text | ข้อความ อาจมี Markdown แบบง่าย เช่น **ตัวหนา** และรายการ 1. |
created_at | ISO-8601 มี timezone |
attachment_url attachment_kind attachment_name | ไฟล์แนบ kind เป็น "image" หรือ "file" |
card | การ์ดสินค้า {"kind": "products", "items": [...]} |
checkout_url | ลิงก์ชำระเงินในข้อความ (ถ้ามี) ทำเป็นปุ่มได้ |
กติกา cursor
- ครั้งแรกไม่ต้องส่ง after จะได้ประวัติทั้งหมด ครั้งละสูงสุด 50 ข้อความ เรียงเก่าไปใหม่
- ครั้งต่อไปส่ง after เป็น cursor ล่าสุด และต้อง URL-encode เพราะมีเครื่องหมาย +
- ไม่มีข้อความใหม่: messages ว่าง และ cursor เป็นค่าเดิมที่ส่งไป
- ได้ครบ 50 ข้อความ: poll ซ้ำทันทีด้วย cursor ใหม่ จนได้น้อยกว่า 50
- ข้อความไม่มี id ถ้าต้องกันซ้ำ ใช้ key
created_at|direction|text|attachment_urlแบบเดียวกับวิดเจ็ตเว็บ - ข้อความที่ผู้ใช้เพิ่งส่งจะกลับมาใน poll ด้วย (direction "in") ถ้าแสดงไปแล้วแบบ optimistic ให้ตัดซ้ำด้วย key ข้างบน
ความถี่ที่แนะนำ
- ขณะหน้าแชทเปิด: ทุก 4 วินาที
- 20 วินาทีหลังผู้ใช้ส่งข้อความ: ทุก 1 วินาที และ poll ครั้งแรกหลังส่งประมาณ 600 ms
- แอปอยู่เบื้องหลัง: หยุด poll แล้วใช้ push (ข้อ 4)
- API ไม่มีสถานะ "กำลังพิมพ์" แสดงเองหลังส่งข้อความได้ สูงสุด 30 วินาทีหรือจนได้คำตอบ
POST/webhooks/web/upload
ส่งรูปหรือไฟล์ เป็น multipart/form-data
| field | บังคับ | หมายเหตุ |
|---|---|---|
token session_id | ✓ | |
file | ✓ | ไม่เกิน 20 MB ห้ามว่าง รับทุกชนิดไฟล์ (content-type ขึ้นต้น image/ แสดงเป็นรูป) |
text | คำอธิบายรูป | |
user_id user_hash | ต้องส่งถ้าผูกสมาชิก |
สำเร็จ: 200 {"ok": true, "conversation_id": "<uuid>", "attachment_url": "<url>"} · error: 401, 429, 400 reserved_session_id, 400 empty file, 413 file too large, 409 prechat_required
POST/webhooks/web/prechat
ฟอร์มก่อนแชท ใช้เมื่อ config.prechat ไม่เป็น null ผู้ใช้ที่ผูกสมาชิกแล้วไม่ต้องทำ
{ "token": "…", "session_id": "…", "name": "Somying", "phone": "0812345678", "email": "", "company": "", "consent": true }- config.prechat.fields บอกว่าแต่ละช่องเป็น off, optional หรือ required
- ถ้า config.prechat.consent_text ไม่ว่าง ต้องส่ง consent: true
- เบอร์โทรต้องมีตัวเลขอย่างน้อย 9 หลัก อีเมลต้องมี @ และโดเมน
- ผลลัพธ์: 200 {"ok": true, "conversation_id"} · 200 {"ok": true, "skipped": true} ถ้าไม่ได้เปิดฟอร์ม · 422 {"ok": false, "error": "invalid", "field", "reason"}
3.6 ขอคุยกับเจ้าหน้าที่
ไม่มี endpoint แยก ให้ส่งข้อความปกติ เช่น "ขอคุยกับเจ้าหน้าที่" หรือทำปุ่ม "ช่วยเหลือ" ที่ส่งข้อความนี้ บอทจะส่งต่อทีมงาน และทีมตอบในบทสนทนาเดียวกัน (sender_type "human")
4. แจ้งเตือนเมื่อมีคำตอบ (Webhook → Push)
ถ้าผู้ใช้ปิดแอปอยู่ แอปจะไม่ได้ poll ตั้ง webhook ไว้ AgenticOS จะแจ้งเซิร์ฟเวอร์ของแอปทุกครั้งที่บอทหรือทีมงานตอบ แล้วเซิร์ฟเวอร์ส่ง FCM/APNs ต่อ AgenticOS ไม่เก็บ device token
4.1 ตั้งค่า
- OmniChat → ช่องทาง → ช่องทางเว็บ → "แจ้งเตือนในแอปมือถือเมื่อมีคำตอบ" ใส่ URL ของเซิร์ฟเวอร์ (https และเป็นโฮสต์สาธารณะ) แล้วบันทึก ระบบจะสร้าง signing key ให้
- คัดลอก signing key ไปเก็บบนเซิร์ฟเวอร์
- กด "ส่งเหตุการณ์ทดสอบ" ต้องเห็นข้อความว่าเซิร์ฟเวอร์ตอบกลับ 200
4.2 สิ่งที่เซิร์ฟเวอร์ได้รับ
| header | ค่า |
|---|---|
X-AgenticOS-Event | message.created (หรือ test จากปุ่มทดสอบ) |
X-AgenticOS-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
User-Agent | AgenticOS-Webhook/1.0 |
{
"event": "message.created",
"channel_id": "75538105-e14a-4f76-bb2d-dd1cc048b0bc",
"conversation_id": "183b01f7-fd8e-4951-a07f-d323fbf6cfac",
"user_id": "member-42",
"session_id": null,
"message": {
"id": "77072727-82a3-4497-ad6d-7582175cf8a4",
"sender": "bot",
"sender_name": "Assistant",
"preview": "The payback period depends on several things…",
"has_attachment": false,
"created_at": "2026-09-30T08:15:02.418221+00:00"
},
"sent_at": "2026-09-30T08:15:04.102118+00:00",
"visitor_active": false
}| key | ความหมาย |
|---|---|
user_id / session_id | ผู้รับ มีค่าอย่างใดอย่างหนึ่ง ใช้หา device token ของผู้ใช้ |
message.sender | "bot" หรือ "agent" (ทีมงาน) |
message.preview | ข้อความตัดไม่เกิน 200 ตัวอักษร ใช้เป็นเนื้อหา notification |
message.id | ใช้กันแจ้งเตือนซ้ำ (collapse key) |
visitor_active | true คือผู้ใช้ poll ภายในประมาณ 100 วินาที น่าจะเปิดแอปอยู่ ไม่ต้องส่ง push ก็ได้ |
- เป็นสัญญาณ ไม่ใช่ข้อมูลเต็ม เนื้อหาเต็มให้แอปดึงผ่าน poll
- คำตอบหลายข้อความภายในประมาณ 1.5 วินาทีจะรวมเป็น event เดียว
- ตอบ 2xx ภายใน 5 วินาที ถ้า timeout หรือ 5xx ระบบลองใหม่อีก 2 ครั้ง (หลัง 2 และ 8 วินาที) ส่วน 4xx ไม่ลองซ้ำ
- ไม่รับประกันลำดับ อาจได้ซ้ำ และไม่ตาม redirect
4.3 ตรวจลายเซ็น (ต้องทำ)
- แยก t และ v1 จาก header
- คำนวณ HMAC-SHA256(signing key, "<t>." + raw body) เป็น hex โดยใช้ raw body ก่อน parse JSON
- เทียบกับ v1 แบบ constant-time ถ้าไม่ตรง ตอบ 401
- ถ้า t เก่ากว่า 5 นาที ให้ปฏิเสธ (กัน replay)
app.post("/omnichat/reply", express.raw({ type: "application/json" }), (req, res) => {
const sig = Object.fromEntries(String(req.get("X-AgenticOS-Signature") || "")
.split(",").map((p) => p.split("=", 2)));
const expected = crypto.createHmac("sha256", process.env.OMNICHAT_PUSH_KEY)
.update(`${sig.t}.`).update(req.body).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(sig.t)) < 300;
if (!fresh || !sig.v1 || sig.v1.length !== expected.length
|| !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig.v1)))
return res.sendStatus(401);
const evt = JSON.parse(req.body);
res.sendStatus(200); // answer first, then push
if (evt.event === "message.created" && !evt.visitor_active) {
sendPush(evt.user_id ?? evt.session_id, {
title: evt.message.sender_name || "New message",
body: evt.message.preview || "📎 Attachment",
collapseKey: evt.message.id,
data: { conversation_id: evt.conversation_id },
});
}
});@app.post("/omnichat/reply")
async def omnichat_reply(request: Request):
raw = await request.body()
parts = dict(p.split("=", 1) for p in request.headers.get("X-AgenticOS-Signature", "").split(","))
expected = hmac.new(PUSH_KEY.encode(), f"{parts.get('t')}.".encode() + raw, hashlib.sha256).hexdigest()
if abs(time.time() - int(parts.get("t", 0))) > 300 or not hmac.compare_digest(expected, parts.get("v1", "")):
raise HTTPException(401)
evt = json.loads(raw)
# … push to evt["user_id"] or evt["session_id"]
return {"ok": True}แอปต้องลงทะเบียน device token กับเซิร์ฟเวอร์ของแอปเอง โดยผูกกับ user_id (หรือ session_id ถ้าไม่ระบุตัวตน)
5. Rate limit
| กลุ่ม | endpoint | จำกัด |
|---|---|---|
| ข้อความ | POST /webhooks/web /prechat | ตามที่ตั้งในองค์กร ค่าเริ่มต้น 20 ครั้ง/นาที ต่อ token ต่อ IP |
| อัปโหลด | /upload | ค่าเดียวกัน แยกนับจากข้อความ |
| poll | /poll | 180 ครั้ง/นาที ต่อ token ต่อ IP |
| config | /config | ไม่จำกัด |
ได้ 429 ให้รอ 2–5 วินาทีแล้วลองใหม่
6. ลำดับการทำงานที่แนะนำในแอป
- เปิดหน้าแชท: GET /config
- ถ้าล็อกอินอยู่: ขอ user_hash จาก backend ของแอป
- GET /poll โดยไม่ส่ง after เพื่อแสดงประวัติ แล้วเก็บ cursor
- ถ้า prechat ไม่เป็น null และ prechat_done เป็น false: แสดงฟอร์ม แล้ว POST /prechat
- ผู้ใช้พิมพ์: แสดงข้อความทันที แล้ว POST /webhooks/web จากนั้น poll ทุก 1 วินาทีเป็นเวลา 20 วินาที
- ออกจากหน้าแชท หรือแอปเข้าเบื้องหลัง: หยุด poll
- ได้ push: เปิดหน้าแชท แล้ว GET /poll?after=<cursor ที่เก็บไว้>
7. ตรวจสอบปัญหา
| อาการ | สาเหตุที่พบบ่อย |
|---|---|
| ส่งได้แต่ไม่เห็นคำตอบ | poll ไม่ได้ส่ง user_id/user_hash ชุดเดียวกับตอนส่ง หรือไม่ได้ URL-encode cursor |
| ผู้ใช้กลายเป็นไม่ระบุตัวตน | user_hash คำนวณผิด หรือยังไม่ได้สร้าง identity key |
| ได้ 409 prechat_required | ต้องส่งฟอร์มก่อนแชท หรือผูกสมาชิกเพื่อข้ามฟอร์ม |
| webhook ไม่มา | URL ไม่ใช่ https, resolve เป็น IP ภายใน, หรือเซิร์ฟเวอร์ตอบไม่ใช่ 2xx ลองกด "ส่งเหตุการณ์ทดสอบ" |
| ลายเซ็นไม่ตรง | ใช้ JSON ที่ parse แล้ว stringify ใหม่แทน raw body หรือใช้ key ผิดตัว (ต้องเป็น signing key) |