Integrations

DTAC · 3CX · Webhooks

Integrations

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

DTAC orktrack

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

เริ่มใช้งาน

  1. Dashboard → Business → Settings → Providers
  2. Tab "Phone Integrations" → Add → kind = dtac
  3. กรอก baseUrl, username, password (default URL คือ onecallvoicerecord.dtac.co.th)
  4. กด Test connection เพื่อตรวจสอบว่าเชื่อมต่อได้
  5. Set as default — ระบบเริ่มดึง recording ให้อัตโนมัติ

3CX cloud

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

ตั้งค่าครั้งเดียวที่ Dashboard → Business → Settings → Providers แล้วระบบจะ sync ให้เอง ไม่ต้องอัปโหลดไฟล์เอง

ถัง MinIO / S3

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

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

เริ่มใช้งาน

  1. Dashboard → Business → Settings → Providers
  2. Tab "Phone Integrations" → Add → kind = minio
  3. กรอก endpoint, access key, secret key, ชื่อ bucket และโฟลเดอร์ (prefix) ที่จะให้ไล่ดู
  4. กด Test connection — ระบบยิง bucketExists จริง จึงตรวจทั้ง credential และการมองเห็นถังในครั้งเดียว
  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 คือก่อนโหลดแม้แต่ไบต์เดียว ส่วนความยาวไม่มีในผลลิสต์ จึงตรวจหลังโหลดไฟล์มา probe เสียค่าโหลด 1 ครั้ง แต่ยังข้ามการเก็บไฟล์ การสร้างแถวสาย การ transcode STT และงาน AI ทุกขั้น

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

ข้อจำกัดที่ควรรู้

  • maxObjectsPerTick (ค่าเริ่มต้น 500) จำกัดจำนวนไฟล์ต่อรอบ การ sync ครั้งแรกของถังใหญ่จึงทยอยเข้าหลายรอบ ไม่ใช่มาทีเดียวหมด
  • การเชื่อมต่อแบบนี้ไม่มีรายชื่อ agent 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

นอกจากใช้ผ่านหน้า UI ของแพลตฟอร์ม ยังเชื่อม Telegram และยิงการแจ้งเตือนผ่าน public API ได้ — เหมาะกับระบบภายนอก (เช่น CRM) ที่ต้องการเป็นเจ้าของ event logic เอง แล้วใช้ CRK เป็นท่อส่ง (delivery pipe)

ออกแบบสำหรับ mapping แบบ 1 tenant ของระบบภายนอก ↔ 1 business ของ CRK: ระบบภายนอกถือ API token ที่ผูกกับ business เดียว แล้วส่ง x-business-id header (หรือใช้ business เริ่มต้นของ token) ในทุก request

สิทธิ์ (scopes)

Scopeใช้กับ
view_notificationsอ่าน status / chats / destinations / automations / preferences
manage_notificationsmint link-code, toggle/unlink chat, test, ส่งการแจ้งเตือน, สร้าง/แก้/ลบ/toggle automation, เชื่อม/ลบ Discord webhook, แก้ preferences

เชื่อม Telegram chat

แพลตฟอร์มใช้ bot Telegram กลางตัวเดียว (token ตั้งที่ฝั่ง server) — แต่ละ business เชื่อม chat ของตัวเองผ่าน flow /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 ถูกผูกกับ business นั้น
  4. poll GET /telegram/chats จนกว่า chat ใหม่จะเข้า list (มี chatId, title, isActive)
  5. ส่งข้อความทดสอบผ่าน POST /telegram/chats/{id}/test เพื่อยืนยัน

เปิด/ปิดการส่งชั่วคราวด้วย PATCH /telegram/chats/{id} ({ isActive: false }) หรือถอด chat ออกด้วย DELETE /telegram/chats/{id} (error chat_not_found 404 ถ้า id ไม่ใช่ของ business นี้)

ยิงการแจ้งเตือน (send-now gateway)

POST /notifications/send รับ payload แล้วส่งออกทันทีตาม channel ที่เลือก — CRK เป็น pure delivery pipe, caller เป็นเจ้าของ event logic:

{
  "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; webhook URL ไม่เคยถูก return ผ่าน API)
  • channel: "in_app" → สร้าง row ใน feed ในแพลตฟอร์ม (ส่ง link ได้ใน data) แล้ว fan-out ผ่าน realtime ไปยังสมาชิกใน business

ถ้า Telegram/Discord API ล้มเหลว จะได้ { ok: false, error: "..." } กลับมาใน body (ไม่ใช่ HTTP error) เพื่อให้ caller retry ได้. Discord webhook ที่ไม่ใช่ของ business นี้แจ้ง discord_webhook_not_found (404)

⚠️ Payload ไม่มี field ของ AI model / provider / ต้นทุน — CRK ไม่เปิดเผยข้อมูลนั้นกับระบบภายนอก

จัดการ automation (กฎการแจ้งเตือน)

CRK เก็บ + ยิง automation เองตอนสายวิเคราะห์เสร็จ. ระบบภายนอกจัดการเป็น "rule" แบบแบน ได้ครบผ่าน public API (CRK compile เป็น 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} — อ่านกฎ (projected กลับเป็น rule); ถ้า graph ถูกแต่งซับซ้อนใน CRK UI เกิน shape ของ simple rule จะได้ rule: null, advanced: true (ให้แก้ใน CRK UI แทน)
  • PUT /notifications/automations/{id} — แทนที่ทั้งกฎ (recompile เป็น graph ใหม่)
  • PATCH /notifications/automations/{id} — เปิด/ปิด ({ enabled })
  • DELETE /notifications/automations/{id} — ลบ (automation_not_found 404)

rule มี trigger เดียว = สายวิเคราะห์เสร็จ. conditions เป็น call field (score/tag/sentiment/direction/...) AND/OR กัน (ว่าง = ทุกสาย); message ใส่ตัวแปร {{score}}, {{customer_phone}}, {{summary}} ฯลฯ; telegramChatIds = chat row id (จาก /telegram/chats), discordWebhookIds = webhook id (จาก /notifications/destinations).

จัดการ 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 — ค่าของผู้ใช้เจ้าของ token (inAppEnabled/sound/desktop/minSeverity/mutedRuleIds)

ผู้ช่วย AI

ถามเป็นภาษาคนแล้วได้คำตอบที่อ้างอิงจากข้อมูลสายของคุณเอง — ผู้ช่วยตัวเดียวกับที่ใช้ในหน้าแชทของแพลตฟอร์ม เปิดให้ n8n flow, CRM หรือระบบหลังบ้านใดก็ตามที่อยากถามมากกว่าจะประกอบ query เอง

POST /assistant/ask

Scopes: view_recording_all · view_recording_self

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

สิ่งที่ endpoint นี้ไม่มีวันคืน

ไม่มีชื่อโมเดล ไม่มีชื่อผู้ให้บริการ ไม่มีจำนวน token ไม่มีต้นทุน — ทั้งใน JSON body และในทุก SSE event · usage.aiProcessed: true คือสิ่งเดียวที่บอกเกี่ยวกับ AI · ข้อนี้บังคับด้วยโครงสร้างของโค้ด ไม่ใช่ด้วยการตรวจทาน: คำตอบแบบ JSON ถูกประกอบใหม่ทีละฟิลด์จาก schema ปิด ส่วนสตรีมถูกกรองทีละ event ก่อนถึงปลายทาง

ข้อผิดพลาด

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

เอกสาร API