Integrations
เชื่อมระบบโทรศัพท์ภายนอก — หรือถังเก็บไฟล์ที่ระบบโทรศัพท์เขียนไฟล์ลงไปอยู่แล้ว — เพื่อดึงสายเข้ามาให้อัตโนมัติ ตั้งค่าครั้งเดียวแล้วระบบจะซิงก์ให้เอง
แหล่งดึงสายทุกแบบคือ ตัวดึงเสียง (connector) — 3CX, DTAC และ MinIO / S3 มีในตัว ส่วนระบบอื่นทีมแพลตฟอร์มจะเพิ่มให้เมื่อมีลูกค้าใช้ โดยฝั่งคุณไม่ต้องแก้อะไร GET /providers/catalog (ดูหน้า Providers) บอกรายชื่อตัวดึงเสียงที่ธุรกิจของคุณใช้ได้ และช่องตั้งค่าที่แต่ละตัวต้องการ (บางตัวเปิดให้เฉพาะบางธุรกิจ) สายที่ดึงเข้ามาด้วยตัวดึงเสียงที่เพิ่มภายหลังจะมี sourceProvider: "script" และ sourceSlug บอกว่าเป็นตัวไหน
การทดสอบการเชื่อมต่อ การดึงรายชื่อ และการดูตัวอย่างไฟล์ ทำในเครื่องรันที่แยกไว้ฝั่งเรา — ไม่ได้ยิงจากเซิร์ฟเวอร์ของหน้าเว็บ ในหน้าเว็บคุณบันทึกการเชื่อมต่อไว้ก่อนโดยยังไม่เริ่มดึงได้ เพื่อตรวจเงื่อนไขการเก็บสายและดูตัวอย่างก่อนที่ระบบจะดึงสายใดเข้ามา ส่วนทาง API ให้ส่ง isActive: false เพื่อทำแบบเดียวกัน
DTAC orktrack
เชื่อม DTAC orktrack แล้วระบบจะดึงสายเข้ามาให้อัตโนมัติตามรอบที่ตั้งไว้ และเก็บข้อมูลล็อกอินแบบเข้ารหัส
เริ่มใช้งาน
- ในหน้าเว็บ ไปที่ ธุรกิจ → ตั้งค่าธุรกิจ → ผู้ให้บริการ
- กด เพิ่มการเชื่อมต่อ แล้วเลือก DTAC One Call
- กรอก baseUrl, username, password (URL เริ่มต้นคือ
onecallvoicerecord.dtac.co.th) - กด ทดสอบการเชื่อมต่อ เพื่อตรวจสอบว่าเชื่อมต่อได้
- บันทึก แล้วเปิดการเชื่อมต่อ — ระบบจะเริ่มดึงสายให้อัตโนมัติ
3CX cloud
เชื่อม 3CX cloud ของคุณ แล้วระบบจะดึงสายเข้ามาให้อัตโนมัติ — AI จะถอดเสียงและสรุปให้ และเรียกดูผ่าน API ได้เหมือนสายอื่น
ตั้งค่าครั้งเดียวที่ ธุรกิจ → ตั้งค่าธุรกิจ → ผู้ให้บริการ แล้วระบบจะ sync ให้เอง ไม่ต้องอัปโหลดไฟล์เอง
ถัง MinIO / S3
ถ้าตู้สาขาของคุณบันทึกไฟล์เสียงลงถังแบบ S3 (MinIO, AWS S3, Wasabi, …) อยู่แล้ว ให้ชี้มาที่ถังนั้นแทนที่จะชี้ไปที่ระบบโทรศัพท์ — ไม่ต้องมี session ล็อกอินและไม่ต้องผ่าน API ของผู้ขายรายไหน เราจะไล่ดูไฟล์ในถังตามรอบแล้วดึงไฟล์เสียงที่เจอเข้ามา ทุกอย่างหลังจากนั้น (ถอดเสียง สรุปด้วย AI ให้คะแนน และการเรียกผ่าน API) เหมือนสายที่ดึงจากตู้สาขาทุกประการ โดยค่า sourceProvider ของสายที่ได้จะเป็น minio
เราอ่านอย่างเดียว ไม่เขียนกลับ ไม่ย้าย ไม่ลบอะไรในถังของคุณ ให้สิทธิ์คีย์แค่ list และ read ก็พอ
เริ่มใช้งาน
- ในหน้าเว็บ ไปที่ ธุรกิจ → ตั้งค่าธุรกิจ → ผู้ให้บริการ
- กด เพิ่มการเชื่อมต่อ แล้วเลือก MinIO / S3
- กรอก endpoint, access key, secret key, ชื่อ bucket และโฟลเดอร์ (
prefix) ที่จะให้ไล่ดู - กด ทดสอบการเชื่อมต่อ — ระบบตรวจทั้งข้อมูลล็อกอินและสิทธิ์มองเห็นถังในครั้งเดียว
- บันทึก แล้วกดดูตัวอย่างไฟล์ เพื่อยืนยันว่าไฟล์ที่ระบบจะดึงคือไฟล์ที่คุณต้องการ
- เปิดการเชื่อมต่อ — ตัวดึงจะเริ่มทำงานในรอบถัดไป
หรือทำแบบเดียวกันผ่าน API — POST /providers โดยใช้ kind: "minio" (ดูโครงสร้าง config ทั้งหมดได้ที่หน้า Providers)
curl -X POST https://phone.mcloud.co.th/api/v1/providers \
-H "Authorization: Bearer crk_..." \
-H "Content-Type: application/json" \
-d '{
"kind": "minio",
"name": "PBX drop folder",
"config": {
"endpoint": "https://s3.example.com:9000",
"accessKey": "AKIA...",
"secretKey": "s3cret",
"bucket": "recordings",
"prefix": "pbx/2026/",
"direction": "in"
}
}'
ไฟล์ไหนที่ระบบดึงเข้ามาจริง
แต่ละรอบมีสามด่านที่คัดไฟล์ออก รู้ลำดับของด่านเหล่านี้แล้วจะช่วยไม่ให้ค่าใช้จ่ายบานปลาย:
- รายการนามสกุลที่อนุญาต (
extensions) — ถังที่ตู้สาขาเขียนลงมักมี CDR csv/json ไฟล์.tmpที่ยังเขียนไม่เสร็จ และบางทีก็มีวิดีโอบันทึกหน้าจอปนอยู่ ระบบจะหยิบเฉพาะนามสกุลไฟล์เสียง ที่เหลือปล่อยไว้เฉย ๆ - ช่วงเวลาย้อนหลัง (
lookbackHoursค่าเริ่มต้น 48) — ระบบดูเฉพาะไฟล์ที่แก้ไขภายในช่วงนี้ จึงไม่ต้องไล่ถังที่เก็บไฟล์หลายปีใหม่ทั้งหมดทุกรอบ ถ้าต้องการดึงไฟล์เก่าย้อนหลัง ต้องทำแยกเป็นอีกงานหนึ่ง - เงื่อนไขการเก็บสาย (
objectFilter) — เลือกเก็บ/ข้ามเป็นรายโฟลเดอร์ และกำหนดช่วงความยาวเสียง ระบบคัดโฟลเดอร์ตอน LIST ของ S3 ก่อนโหลดไฟล์แม้แต่ไบต์เดียว ส่วนความยาวเสียงไม่มีในผล LIST จึงต้องโหลดไฟล์มาตรวจ (probe) ก่อน เสียค่าโหลด 1 ครั้ง แต่ยังข้ามการเก็บไฟล์ การสร้างสาย การแปลงไฟล์ (transcode) การถอดเสียง และงาน AI ทุกขั้น
ไฟล์ในถังไม่มีข้อมูลผู้โทร และไม่บอกว่าเป็นสายเข้าหรือสายออก จึงต้องกำหนด direction ไว้ที่การเชื่อมต่อ แล้วระบบจะใส่ค่านี้ให้ทุกสายที่ดึงเข้ามา ส่วน path เต็มของไฟล์ ระบบจะเก็บเป็นโน้ตของสาย การตั้งเงื่อนไขรายโฟลเดอร์จึงคุ้มค่า เพราะในโฟลเดอร์ที่ตู้สาขาเขียนไฟล์ลง path มักบอกสาขา เบอร์ภายใน หรือวันที่อยู่แล้ว
ข้อจำกัดที่ควรรู้
maxObjectsPerTick(ค่าเริ่มต้น 500) จำกัดจำนวนไฟล์ต่อรอบ การ sync ครั้งแรกของถังใหญ่จึงทยอยเข้าหลายรอบ ไม่ใช่มาทีเดียวหมด- การเชื่อมต่อแบบนี้ไม่มีรายชื่อพนักงาน
GET /providers/{id}/peopleจึงตอบprovider_kind_not_supported(422) — ให้ใช้การดูตัวอย่างไฟล์ในหน้าเว็บเพื่อหา path ของโฟลเดอร์แทน - ธุรกิจเดียวเชื่อมได้หลายถัง แต่ละถังดึงตามรอบของตัวเอง
Webhooks
ใช้งานได้แล้วในชื่อ Callbacks — เรา POST ไป endpoint ของคุณเมื่อสายวิเคราะห์เสร็จ (analysis_completed) หรือวิเคราะห์ไม่สำเร็จ (analysis_failed) พร้อมลายเซ็น HMAC ให้ตรวจสอบได้ ตั้งค่าผ่าน GET / PUT / DELETE /api/v1/callbacks — รายละเอียด payload, การตรวจลายเซ็น, การส่งซ้ำ และกติกาของ URL อยู่ที่หน้า Callbacks
Telegram & Notification gateway
นอกจากตั้งค่าในหน้าเว็บของแพลตฟอร์มแล้ว คุณยังเชื่อม Telegram และส่งการแจ้งเตือนผ่าน public API ได้ — เหมาะกับระบบภายนอก (เช่น CRM) ที่อยากกำหนดเองว่าจะแจ้งเตือนเมื่อไร แล้วใช้ CRK เป็นแค่ช่องทางส่ง
แพ็กเกจ: ธุรกิจที่แพ็กเกจไม่รวมการส่งผลการวิเคราะห์ออก สร้างหรือแทนที่กฎไม่ได้ (
POST/PUT /notifications/automationsตอบ403 feature_not_in_package) และระบบจะไม่ส่งขั้นตอนส่งทาง Telegram Discord และ Webhook ส่วนการแจ้งเตือนในแอปยังส่งตามปกติ ดู เริ่มต้นใช้งาน → Endpoint ที่ขึ้นกับแพ็กเกจ
ออกแบบให้ 1 tenant ของระบบภายนอก ตรงกับ 1 ธุรกิจใน CRK: ระบบภายนอกถือ API key ที่ผูกกับธุรกิจเดียว แล้วส่ง header x-business-id (หรือใช้ธุรกิจเริ่มต้นของ API key) ในทุก request
สิทธิ์ (scopes)
| Scope | ใช้กับ |
|---|---|
view_notifications | อ่าน status / chats / destinations / automations / preferences |
manage_notifications | ออก link-code · เปิด/ปิดหรือยกเลิกการเชื่อมต่อ chat · ส่งข้อความทดสอบ · ส่งการแจ้งเตือน · สร้าง/แก้/ลบ/เปิดปิด automation · เชื่อม/ลบ Discord webhook · แก้ preferences |
เชื่อม Telegram chat
แพลตฟอร์มใช้บอท Telegram กลางตัวเดียว (token ของบอทตั้งไว้ฝั่งเซิร์ฟเวอร์) — แต่ละธุรกิจเชื่อม chat ของตัวเองผ่านขั้นตอน /start <code>:
- เรียก
POST /telegram/link-code→ ได้{ code, deepLink, expiresInSec }(code อายุ 15 นาที) - ระบบภายนอก render QR code หรือ deep link
https://t.me/<bot>?start=<code>ให้ผู้ใช้สแกน/กด - ผู้ใช้กด start ใน Telegram แล้ว chat จะผูกกับธุรกิจนั้น
- poll
GET /telegram/chatsจนกว่า chat ใหม่จะขึ้นในรายการ (มีchatId,title,isActive) - ส่งข้อความทดสอบผ่าน
POST /telegram/chats/{id}/testเพื่อยืนยัน
เปิด/ปิดการส่งชั่วคราวด้วย PATCH /telegram/chats/{id} ({ isActive: false }) หรือยกเลิกการเชื่อมต่อ chat ด้วย DELETE /telegram/chats/{id} (ได้ chat_not_found 404 ถ้า id ไม่ใช่ของธุรกิจนี้)
ยิงการแจ้งเตือน (send-now gateway)
POST /notifications/send รับ payload แล้วส่งออกทันทีทาง channel ที่เลือก — CRK ทำหน้าที่ส่งอย่างเดียว ส่วนผู้เรียกเป็นคนกำหนดว่าจะแจ้งเตือนเมื่อไร:
{
"channel": "telegram",
"chatId": "<id จาก /telegram/chats>",
"title": "Lead ใหม่: คุณสมชาย",
"body": "เบอร์ 08x-xxx-xxxx สนใจบ้านเดี่ยว โครงการ A",
"link": "https://crm.example.com/leads/123",
"severity": "info"
}
channel: "telegram"→ ต้องส่งchatId(เอาจากGET /telegram/chatsหรือGET /notifications/destinations)channel: "discord"→ ต้องส่งwebhookId(เอาจากGET /notifications/destinations; API ไม่ส่ง webhook URL กลับมาเลย)channel: "in_app"→ สร้างรายการในฟีดการแจ้งเตือนของแพลตฟอร์ม (ส่งlinkได้ในdata) แล้วส่งแบบ realtime ไปยังสมาชิกทุกคนในธุรกิจ
ถ้าเรียก Telegram/Discord API ไม่สำเร็จ จะได้ { ok: false, error: "..." } กลับมาใน body (ไม่ใช่ HTTP error) เพื่อให้ผู้เรียกลองใหม่ได้ ส่วน Discord webhook ที่ไม่ใช่ของธุรกิจนี้จะได้ discord_webhook_not_found (404)
⚠️ Payload ไม่มีฟิลด์ชื่อโมเดล AI ผู้ให้บริการ หรือต้นทุน — CRK ไม่เปิดเผยข้อมูลเหล่านี้กับระบบภายนอก
จัดการ automation (กฎการแจ้งเตือน)
CRK เก็บ automation ไว้และสั่งทำงานเองเมื่อสายวิเคราะห์เสร็จ ระบบภายนอกจัดการได้ครบผ่าน public API ในรูป "rule" แบบแบน (CRK แปลงเป็น flow graph ให้เอง — คุณไม่ต้องรู้โครงสร้าง graph):
GET /notifications/automations— รายการ automation พร้อมสถิติการส่ง (sent/failed/lastRunAt)POST /notifications/automations— สร้างกฎใหม่จาก rule{ name, conditions[], message, telegramChatIds[], discordWebhookIds[] }→ คืน{ id }(201)GET /notifications/automations/{id}— อ่านกฎ (แปลงกลับจาก graph เป็น rule) ถ้า graph ถูกแก้ในหน้าเว็บของ CRK จนซับซ้อนเกินรูปแบบ rule ธรรมดา จะได้rule: null, advanced: true(ให้แก้ในหน้าเว็บของ CRK แทน)PUT /notifications/automations/{id}— แทนที่ทั้งกฎ (recompile เป็น graph ใหม่)PATCH /notifications/automations/{id}— เปิด/ปิด ({ enabled })DELETE /notifications/automations/{id}— ลบ (automation_not_found404)
rule มี trigger เดียวคือสายวิเคราะห์เสร็จ · conditions คือฟิลด์ของสาย (score/tag/sentiment/direction/...) ที่เชื่อมกันด้วย AND/OR (ถ้าว่าง หมายถึงทุกสาย) · message ใส่ตัวแปร {{score}}, {{customer_phone}}, {{summary}} ฯลฯ ได้ · telegramChatIds คือ id ของ chat (จาก /telegram/chats) และ discordWebhookIds คือ id ของ webhook (จาก /notifications/destinations)
เงื่อนไข Flag ประจำสาย — ตรวจ Flag ที่สายได้รับ ใช้กับ trigger เดิมได้เลย เพราะการตรวจ Flag เสร็จก่อนที่ automation จะทำงาน
| field | op | value |
|---|---|---|
flag | has / not_has | key ของ Flag เช่น churn_risk — ดู key ทั้งหมดได้จาก GET /recordings/insight-config |
flag_level | has / not_has | ระดับ Flag critical / warning / positive / info (สายมี Flag ระดับนี้อย่างน้อยหนึ่ง Flag) |
- นับเฉพาะ Flag ที่เชื่อถือได้ คือ Flag ที่มีประโยคหลักฐานในสาย หรือที่คนติดเอง ส่วน Flag ที่มีคนกดว่าไม่ถูกต้อง และ Flag ของ AI ที่หาประโยคหลักฐานในสายไม่เจอ จะไม่นับ
- สายที่ไม่ได้ตรวจ Flag (ปิดการตรวจไว้ สายสั้นเกินไป หรือตรวจไม่สำเร็จ) นับว่าไม่มี Flag เลย:
hasไม่ผ่าน ส่วนnot_hasผ่าน flagต้องเป็น key ของธุรกิจนี้ (Flag ที่เก็บถาวรแล้วก็ใช้ได้) ไม่อย่างนั้นจะได้422 flow_flag_unknown— กันไม่ให้ key ที่พิมพ์ผิดทำให้not_hasส่งทุกสาย- ใส่
{{flags}}ในmessageเพื่อแสดงชื่อ Flag ของสาย คั่นด้วย ", " เรียงจากระดับที่ด่วนที่สุดก่อน (ว่างเมื่อสายไม่มี Flag)
{
"name": "แจ้งทันทีเมื่อเจอ Flag เร่งด่วน",
"enabled": true,
"conditions": [{ "field": "flag_level", "op": "has", "value": "critical" }],
"message": "Flag เร่งด่วน: {{flags}} · เบอร์ {{customer_phone}} ({{callcenter}}) {{detail_link}}",
"telegramChatIds": ["<id จาก /telegram/chats>"]
}
จัดการ Discord webhook (ปลายทาง)
POST /notifications/discord— เชื่อม webhook ใหม่{ name, webhookUrl }(ต้องเป็น URL ของ discord(app).com มิฉะนั้นแจ้งdiscord_invalid_webhook422) → คืน{ id }(201)DELETE /notifications/discord/{id}— ลบ webhook (discord_webhook_not_found404)
preferences
GET /notifications/preferencesและPATCH /notifications/preferences— ค่าตั้งของผู้ใช้ที่เป็นเจ้าของ API key (inAppEnabled/sound/desktop/minSeverity/mutedRuleIds)
ผู้ช่วย AI
ถามเป็นภาษาคนแล้วได้คำตอบที่อ้างอิงจากข้อมูลสายของคุณเอง — ผู้ช่วยตัวเดียวกับที่ใช้ในหน้าแชทของแพลตฟอร์ม เปิดให้ n8n flow, CRM หรือระบบหลังบ้านใดก็ตามที่อยากถามมากกว่าจะประกอบ query เอง
POST /assistant/ask
Scopes: view_recording_all · view_recording_self
แพ็กเกจ: ตอบ
403 feature_not_in_packageถ้าแพ็กเกจของธุรกิจไม่รวมการส่งผลการวิเคราะห์ออก ดู เริ่มต้นใช้งาน → Endpoint ที่ขึ้นกับแพ็กเกจ
| Field | Type | Note |
|---|---|---|
question | string 1-4000 | required · ตัดช่องว่างหัวท้ายให้ |
model | string ≤120 | optional — ระบุรุ่นที่อยากใช้ได้ แต่ระบบจะไม่ส่งค่านี้กลับ |
conversationId | uuid | optional — คุยต่อจากเธรดเดิม ต้องเป็นเธรดของธุรกิจนี้ |
stream | boolean | default false · ถ้า true จะตอบเป็น Server-Sent Events แทน JSON |
curl -X POST https://phone.mcloud.co.th/api/v1/assistant/ask \
-H "Authorization: Bearer crk_..." \
-H "Content-Type: application/json" \
-d '{"question":"สรุปสายเมื่อวานให้หน่อย"}'
{
"answer": "เมื่อวาน (13 ส.ค.) มีสายเข้า 41 สาย รับได้ 36 ...",
"sources": [
{ "title": "สายจาก 08x-xxx-xxxx เวลา 10:14", "url": "https://phone.mcloud.co.th/app/recordings/0192b9...", "snippet": "ลูกค้าสอบถามโปรโมชั่น..." }
],
"conversationId": "0192d4...",
"usage": { "aiProcessed": true }
}
เก็บ conversationId ไว้แล้วส่งกลับมาในคำถามถัดไป เพื่อคุยต่อในเธรดเดิม
แบบสตรีม
เมื่อส่ง stream: true response จะเป็น text/event-stream; charset=utf-8 (cache-control: no-cache, no-transform, x-accel-buffering: no) · แต่ละบล็อกเป็น data: <json> และคง id: ไว้ให้ด้วย เมื่อการเชื่อมต่อหลุดกลางคัน จึงต่อสตรีมด้วย Last-Event-ID ได้
ระบบส่ง event ออกมาแค่ชนิดเหล่านี้: start, text, sources, title, done, error, ping · นี่เป็น allowlist — event ใดก็ตามที่บอกว่าคำตอบสร้างขึ้นอย่างไร (ขั้นตอนคิด การเรียก tool จำนวนโทเคน การสลับโมเดล) ระบบจะตัดทิ้งทั้ง event ไม่ใช่แค่กรองบางฟิลด์ · event ชนิดใหม่จากต้นทางจึงหลุดออกมาเองไม่ได้
สิ่งที่ endpoint นี้ไม่มีวันคืน
ไม่มีชื่อโมเดล ไม่มีชื่อผู้ให้บริการ ไม่มีจำนวนโทเคน ไม่มีต้นทุน — ทั้งใน JSON body และในทุก SSE event · usage.aiProcessed: true คือสิ่งเดียวที่บอกเกี่ยวกับ AI · ข้อนี้บังคับด้วยโครงสร้างของโค้ด ไม่ใช่ด้วยการตรวจทาน: ระบบประกอบคำตอบแบบ JSON ใหม่ทีละฟิลด์จาก schema ปิด และกรองสตรีมทีละ event ก่อนถึงปลายทาง
ข้อผิดพลาด
| Status | Code | ความหมาย |
|---|---|---|
| 400 | validation_failed | ฟิลด์ใน body ไม่ผ่านการตรวจ (issues บอกว่าฟิลด์ไหน) |
| 403 | insufficient_scope | API key ไม่มี view scope ตัวใดเลย |
| 404 | conversation_not_found | ไม่มีเธรดนี้ หรือ เธรดเป็นของธุรกิจอื่น (เพราะถ้าตอบ 403 จะเท่ากับยืนยันว่า id นั้นมีอยู่จริง) |
| 422 | invalid_payload | body ไม่ใช่ JSON object |
| 429 | rate_limited | ถามถี่เกินไป — มี header retry-after และ x-ratelimit-limit กำกับ |
ผู้ช่วยอาจใช้งานไม่ได้ชั่วคราวโดยไม่ใช่ความผิดของผู้เรียก กรณีเหล่านี้จึงมีรหัสของตัวเอง และไม่มีทางเป็น 500: 503 agent_host_unreachable / agent_host_not_configured / agent_host_rejected / agent_host_error, 504 agent_host_timeout, 429 agent_host_rate_limited, 400 agent_host_bad_request, 499 request_cancelled · body เป็น { "error": { "code": "...", "retryable": true|false } } — ให้ลองใหม่เฉพาะตอน retryable เป็น true และเว้นระยะแบบ backoff
การจำกัดอัตราคิดต่อ API key ค่าเริ่มต้น 30 คำถามต่อนาที · การถาม 1 ครั้งคือการรันโมเดลและวนเรียก tool ให้คิดว่าเป็นคำขอที่แพง — ถามครั้งเดียวแล้วเก็บผลไว้ใช้ ดีกว่าถามทีละแถว