Integrations

DTAC · 3CX · Webhook

Integrations

เชื่อมระบบโทรศัพท์ภายนอก — หรือถังเก็บไฟล์ที่ระบบโทรศัพท์เขียนไฟล์ลงไปอยู่แล้ว — เพื่อดึงสายเข้ามาให้อัตโนมัติ ตั้งค่าครั้งเดียวแล้วระบบจะซิงก์ให้เอง

แหล่งดึงสายทุกแบบคือ ตัวดึงเสียง (connector) — 3CX, DTAC และ MinIO / S3 มีในตัว ส่วนระบบอื่นทีมแพลตฟอร์มจะเพิ่มให้เมื่อมีลูกค้าใช้ โดยฝั่งคุณไม่ต้องแก้อะไร GET /providers/catalog (ดูหน้า Providers) บอกรายชื่อตัวดึงเสียงที่ธุรกิจของคุณใช้ได้ และช่องตั้งค่าที่แต่ละตัวต้องการ (บางตัวเปิดให้เฉพาะบางธุรกิจ) สายที่ดึงเข้ามาด้วยตัวดึงเสียงที่เพิ่มภายหลังจะมี sourceProvider: "script" และ sourceSlug บอกว่าเป็นตัวไหน

การทดสอบการเชื่อมต่อ การดึงรายชื่อ และการดูตัวอย่างไฟล์ ทำในเครื่องรันที่แยกไว้ฝั่งเรา — ไม่ได้ยิงจากเซิร์ฟเวอร์ของหน้าเว็บ ในหน้าเว็บคุณบันทึกการเชื่อมต่อไว้ก่อนโดยยังไม่เริ่มดึงได้ เพื่อตรวจเงื่อนไขการเก็บสายและดูตัวอย่างก่อนที่ระบบจะดึงสายใดเข้ามา ส่วนทาง API ให้ส่ง isActive: false เพื่อทำแบบเดียวกัน

DTAC orktrack

เชื่อม DTAC orktrack แล้วระบบจะดึงสายเข้ามาให้อัตโนมัติตามรอบที่ตั้งไว้ และเก็บข้อมูลล็อกอินแบบเข้ารหัส

เริ่มใช้งาน

  1. ในหน้าเว็บ ไปที่ ธุรกิจ → ตั้งค่าธุรกิจ → ผู้ให้บริการ
  2. กด เพิ่มการเชื่อมต่อ แล้วเลือก DTAC One Call
  3. กรอก baseUrl, username, password (URL เริ่มต้นคือ onecallvoicerecord.dtac.co.th)
  4. กด ทดสอบการเชื่อมต่อ เพื่อตรวจสอบว่าเชื่อมต่อได้
  5. บันทึก แล้วเปิดการเชื่อมต่อ — ระบบจะเริ่มดึงสายให้อัตโนมัติ

3CX cloud

เชื่อม 3CX cloud ของคุณ แล้วระบบจะดึงสายเข้ามาให้อัตโนมัติ — AI จะถอดเสียงและสรุปให้ และเรียกดูผ่าน API ได้เหมือนสายอื่น

ตั้งค่าครั้งเดียวที่ ธุรกิจ → ตั้งค่าธุรกิจ → ผู้ให้บริการ แล้วระบบจะ sync ให้เอง ไม่ต้องอัปโหลดไฟล์เอง

ถัง MinIO / S3

ถ้าตู้สาขาของคุณบันทึกไฟล์เสียงลงถังแบบ S3 (MinIO, AWS S3, Wasabi, …) อยู่แล้ว ให้ชี้มาที่ถังนั้นแทนที่จะชี้ไปที่ระบบโทรศัพท์ — ไม่ต้องมี session ล็อกอินและไม่ต้องผ่าน API ของผู้ขายรายไหน เราจะไล่ดูไฟล์ในถังตามรอบแล้วดึงไฟล์เสียงที่เจอเข้ามา ทุกอย่างหลังจากนั้น (ถอดเสียง สรุปด้วย AI ให้คะแนน และการเรียกผ่าน API) เหมือนสายที่ดึงจากตู้สาขาทุกประการ โดยค่า sourceProvider ของสายที่ได้จะเป็น minio

เราอ่านอย่างเดียว ไม่เขียนกลับ ไม่ย้าย ไม่ลบอะไรในถังของคุณ ให้สิทธิ์คีย์แค่ list และ read ก็พอ

เริ่มใช้งาน

  1. ในหน้าเว็บ ไปที่ ธุรกิจ → ตั้งค่าธุรกิจ → ผู้ให้บริการ
  2. กด เพิ่มการเชื่อมต่อ แล้วเลือก MinIO / S3
  3. กรอก endpoint, access key, secret key, ชื่อ bucket และโฟลเดอร์ (prefix) ที่จะให้ไล่ดู
  4. กด ทดสอบการเชื่อมต่อ — ระบบตรวจทั้งข้อมูลล็อกอินและสิทธิ์มองเห็นถังในครั้งเดียว
  5. บันทึก แล้วกดดูตัวอย่างไฟล์ เพื่อยืนยันว่าไฟล์ที่ระบบจะดึงคือไฟล์ที่คุณต้องการ
  6. เปิดการเชื่อมต่อ — ตัวดึงจะเริ่มทำงานในรอบถัดไป

หรือทำแบบเดียวกันผ่าน 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>:

  1. เรียก POST /telegram/link-code → ได้ { code, deepLink, expiresInSec } (code อายุ 15 นาที)
  2. ระบบภายนอก render QR code หรือ deep link https://t.me/<bot>?start=<code> ให้ผู้ใช้สแกน/กด
  3. ผู้ใช้กด start ใน Telegram แล้ว chat จะผูกกับธุรกิจนั้น
  4. poll GET /telegram/chats จนกว่า chat ใหม่จะขึ้นในรายการ (มี chatId, title, isActive)
  5. ส่งข้อความทดสอบผ่าน 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_found 404)

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 จะทำงาน

fieldopvalue
flaghas / not_haskey ของ Flag เช่น churn_risk — ดู key ทั้งหมดได้จาก GET /recordings/insight-config
flag_levelhas / 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_webhook 422) → คืน { id } (201)
  • DELETE /notifications/discord/{id} — ลบ webhook (discord_webhook_not_found 404)

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 ที่ขึ้นกับแพ็กเกจ

FieldTypeNote
questionstring 1-4000required · ตัดช่องว่างหัวท้ายให้
modelstring ≤120optional — ระบุรุ่นที่อยากใช้ได้ แต่ระบบจะไม่ส่งค่านี้กลับ
conversationIduuidoptional — คุยต่อจากเธรดเดิม ต้องเป็นเธรดของธุรกิจนี้
streambooleandefault 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 ก่อนถึงปลายทาง

ข้อผิดพลาด

StatusCodeความหมาย
400validation_failedฟิลด์ใน body ไม่ผ่านการตรวจ (issues บอกว่าฟิลด์ไหน)
403insufficient_scopeAPI key ไม่มี view scope ตัวใดเลย
404conversation_not_foundไม่มีเธรดนี้ หรือ เธรดเป็นของธุรกิจอื่น (เพราะถ้าตอบ 403 จะเท่ากับยืนยันว่า id นั้นมีอยู่จริง)
422invalid_payloadbody ไม่ใช่ JSON object
429rate_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 ให้คิดว่าเป็นคำขอที่แพง — ถามครั้งเดียวแล้วเก็บผลไว้ใช้ ดีกว่าถามทีละแถว