Providers

เชื่อมต่อโทรศัพท์ 3CX · DTAC

Providers

ข้อมูล credential ของแหล่งดึงสายเข้าระบบ — ช่องทางที่ recording ไหลเข้ามา รองรับสามชนิด: threecx (3CX PBX), dtac (DTAC carrier) และ minio (bucket แบบ S3 ที่ตู้สาขาของคุณเขียนไฟล์ลงไปอยู่แล้ว)

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

Account key: ทุก request ด้านล่างต้องส่ง X-Business-Id: <business id> เพื่อเลือก business ที่จะทำงานด้วย (ถ้าไม่ส่งจะได้ 400 business_required) business key ไม่สนใจ header นี้ — มันผูกกับ business เดียวอยู่แล้ว ดูเพิ่มที่ Getting started → API Key ระดับบัญชี

Secret เป็นแบบ write-only provider เก็บ credential ไว้ — password สำหรับชนิดโทรศัพท์ และ secretKey สำหรับ bucket คุณส่งค่าพวกนี้ตอน create/update ได้ แต่ GET จะไม่คืนกลับมา — response ของการอ่านจะแสดงเฉพาะส่วนที่ไม่ลับ (baseUrl + username หรือบล็อกของ bucket ซึ่งรวม accessKey ด้วย) ส่วน list endpoint จะแสดง preview ที่ถูกปิดบังแทน

GET /providers

ดูแหล่งดึงสายที่ตั้งค่าไว้สำหรับ business นี้

Scopes: manage_provider_config

curl https://phone.mcloud.co.th/api/v1/providers \
  -H "Authorization: Bearer crk_..."
{
  "items": [
    {
      "id": "0192f0...",
      "kind": "threecx",
      "name": "Main PBX",
      "isActive": true,
      "preview": "apiuser @ pbx.example.com",
      "createdAt": "2026-05-01T00:00:00Z"
    },
    {
      "id": "0192f1...",
      "kind": "minio",
      "name": "PBX drop folder",
      "isActive": true,
      "preview": "recordings/pbx/2026 @ s3.example.com:9000",
      "createdAt": "2026-08-01T00:00:00Z"
    }
  ]
}

preview คือคำใบ้ที่ปิดบังค่าลับไว้ รูปแบบขึ้นกับชนิด: ระบบโทรศัพท์เป็น username @ baseUrl ส่วน bucket เป็น bucket/prefix @ endpoint — ไม่มีค่าลับอยู่ในนั้น

POST /providers

สร้าง provider — รูปร่างของ object config ขึ้นกับ kind (ดูด้านล่าง)

Scopes: manage_provider_config

ParamTypeNote
kind"threecx" | "dtac" | "minio"required
namestring (2-60)required
configobjectrequired — รูปร่างขึ้นกับ kind
isActivebooleanoptional (default true)

config สำหรับ threecx:

FieldTypeNote
baseUrlurlrequired
usernamestringrequired
passwordstringrequired (write-only)
headlessbooleanoptional

config สำหรับ dtac:

FieldTypeNote
baseUrlurloptional
usernamestringrequired
passwordstringrequired (write-only)
pollIntervalMinutesint 5-1440optional

config สำหรับ minio:

bucket แบบ S3 (MinIO, AWS S3, Wasabi, …) ที่ตู้สาขาของคุณทิ้งไฟล์เสียงลงไปอยู่แล้ว — เราอ่านอย่างเดียว ไม่เขียนกลับ

FieldTypeNote
endpointstringrequired — ใส่เป็น host[:port] หรือ URL เต็ม http(s):// ก็ได้ ถ้ามี scheme จะใช้ scheme ตัดสินว่าใช้ TLS หรือไม่
accessKeystringrequired
secretKeystringrequired (write-only)
bucketstringrequired
useSSLbooleanoptional (default false) — ใช้เฉพาะตอน endpoint ไม่มี scheme
prefixstringoptional — โฟลเดอร์ที่จะไล่ดู เช่น recordings/2026/ เว้นว่าง = ทั้ง bucket
regionstringoptional
extensionsstring[]optional — นามสกุลไฟล์เสียงที่จะดึง ตัวพิมพ์เล็กและไม่ต้องมีจุด ไม่ใส่ = ใช้รายการเสียงทั้งหมด (wav, mp3, m4a, ogg, opus, flac, …) เพราะ bucket ที่ตู้สาขาเขียนลงมักมี CDR csv/json และไฟล์ .tmp ปนอยู่ด้วย จึงต้องเป็น allowlist ไม่ใช่ blocklist
direction"in" | "out"optional (default "in") — bucket ไม่มีข้อมูลทิศทางสาย จึงต้องประกาศเองว่าโฟลเดอร์นี้เก็บสายแบบไหน แล้วค่านี้จะถูกประทับลงทุกสายที่ดึงเข้ามา
lookbackHoursint 1-720optional (default 48) — พิจารณาเฉพาะไฟล์ที่แก้ไขภายในช่วงย้อนหลังนี้ bucket ที่เก็บประวัติหลายปีจึงไม่ถูกไล่ใหม่ทั้งก้อนทุกรอบ
pollIntervalMinutesint 5-1440optional (default 15)
maxObjectsPerTickint 1-5000optional (default 500) — เพดานกันงานบานในรอบเดียว

เงื่อนไขการเก็บสาย — config.agentFilter (เฉพาะชนิดโทรศัพท์, optional):

เก็บเฉพาะสายของ agent ที่เลือก ทำงาน ก่อนดาวน์โหลด สายที่ถูกกรองออกจะไม่ถูก transcode วิเคราะห์ หรือคิดเงิน ไม่ใส่ (หรือใช้ mode: "all") = เก็บทุกสาย ดูค่า keys ที่ใช้ได้จาก GET /providers/{id}/people

FieldTypeNote
mode"all" | "include" | "exclude"default "all"
keysstring[]agent ที่จะเก็บ/ยกเว้น — 3CX: extension · DTAC: user id
directions("in" | "out")[]optional — ไม่ใส่ = ทั้งสองทิศ
minDurationSecint 0-3600optional — ตัดสายที่สั้นกว่านี้
maxDurationSecint 0-28800optional — ตัดสายที่ยาวกว่านี้ ถ้าใส่ค่าน้อยกว่า minDurationSec ระบบจะไม่ใช้ค่านี้ (ไม่งั้นจะไม่เหลือสายเลย)

เงื่อนไขการเก็บสาย — config.objectFilter (เฉพาะ minio, optional):

bucket ไม่มีรายชื่อ agent สิ่งที่ตั้งเงื่อนไขได้จริงจึงมีสองอย่าง: ไฟล์ อยู่ที่ไหน และเสียง ยาวเท่าไร สองส่วนนี้บังคับใช้คนละจุดและมีผลกับค่าใช้จ่ายต่างกัน — folders ถูกคัดตอน LIST ของ S3 คือก่อนโหลดแม้แต่ไบต์เดียว ส่วนความยาวไม่มีอยู่ในผลลิสต์ จึงตรวจได้หลังโหลดไฟล์มา probe เท่านั้น เสียค่าโหลด 1 ครั้ง แต่ยังข้ามการเก็บไฟล์ การสร้างแถวสาย การ transcode STT และงาน AI ทุกขั้น

FieldTypeNote
mode"all" | "include" | "exclude"default "all"
foldersstring[]โฟลเดอร์ที่จะเก็บ/ยกเว้น — ใส่เป็น path เต็ม (pbx/2026/08), path ที่เทียบจาก prefix (08 เมื่อ prefix คือ pbx/2026/) หรือชื่อโฟลเดอร์เปล่า ๆ (sales) ซึ่งจะแมตช์ชั้นนั้นที่ความลึกใดก็ได้
minDurationSecint 0-28800optional — ตัดเสียงที่สั้นกว่านี้
maxDurationSecint 0-28800optional — ตัดเสียงที่ยาวกว่านี้ ถ้าใส่ค่าน้อยกว่า minDurationSec ระบบจะไม่ใช้ค่านี้
curl -X POST https://phone.mcloud.co.th/api/v1/providers \
  -H "Authorization: Bearer crk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "threecx",
    "name": "Main PBX",
    "config": {
      "baseUrl": "https://pbx.example.com",
      "username": "apiuser",
      "password": "s3cret"
    },
    "isActive": true
  }'
{ "id": "0192f0..." }

การสร้างแหล่งแบบ bucket เขียนแบบนี้:

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",
      "lookbackHours": 48,
      "pollIntervalMinutes": 15
    },
    "isActive": true
  }'

GET /providers/{id}

ดึง provider หนึ่งตัว — จะไม่คืน password หรือ secretKey มีแค่ส่วนที่ไม่ลับ

Scopes: manage_provider_config

ErrorStatusเมื่อไหร่
provider_not_found404ไม่มี provider นี้ใน business นี้
provider_kind_not_supported422kind ที่เก็บไว้ไม่ใช่แหล่งดึงสาย (เช่นเป็นแถวของโมเดล AI)

response มีทั้งสองฝั่งเสมอ ฝั่งที่ไม่เกี่ยวกับ kind นั้นจะเป็น null — client จึงไม่ต้องแยกเงื่อนไขตาม kind เพียงเพื่ออ่านค่า ระบบโทรศัพท์จะเติม baseUrl / username / agentFilter แล้วปล่อย minio เป็น null:

{
  "id": "0192f0...",
  "kind": "threecx",
  "name": "Main PBX",
  "isActive": true,
  "baseUrl": "https://pbx.example.com",
  "username": "apiuser",
  "agentFilter": {
    "mode": "include",
    "keys": ["12110", "12117"],
    "directions": ["in", "out"],
    "minDurationSec": 10,
    "maxDurationSec": 3600
  },
  "minio": null
}

ส่วน bucket จะกลับกัน — คืนทุกอย่างยกเว้น secretKey:

{
  "id": "0192f1...",
  "kind": "minio",
  "name": "PBX drop folder",
  "isActive": true,
  "baseUrl": null,
  "username": null,
  "agentFilter": null,
  "minio": {
    "endpoint": "https://s3.example.com:9000",
    "useSSL": true,
    "accessKey": "AKIA...",
    "bucket": "recordings",
    "prefix": "pbx/2026/",
    "region": null,
    "extensions": ["wav", "mp3", "m4a"],
    "direction": "in",
    "lookbackHours": 48,
    "pollIntervalMinutes": 15,
    "maxObjectsPerTick": 500,
    "objectFilter": {
      "mode": "include",
      "folders": ["sales"],
      "minDurationSec": 10
    }
  }
}

PATCH /providers/{id}

แก้ provider — config จะถูก merge กับค่าที่เก็บไว้ ดังนั้นส่งเฉพาะ key ที่ต้องการเปลี่ยนได้ (เช่นเปลี่ยน password โดยไม่ต้องส่ง baseUrl ซ้ำ)

Scopes: manage_provider_config

ParamTypeNote
namestring (2-60)optional
configobjectoptional — merge เข้ากับ config ที่เก็บไว้
isActivebooleanoptional
curl -X PATCH https://phone.mcloud.co.th/api/v1/providers/$ID \
  -H "Authorization: Bearer crk_..." \
  -H "Content-Type: application/json" \
  -d '{"config":{"password":"r0tated"}}'

ตั้งเงื่อนไขการเก็บสายด้วยวิธีเดียวกัน — ส่ง config.agentFilter (merge เข้าไป จึงไม่กระทบ credential):

curl -X PATCH https://phone.mcloud.co.th/api/v1/providers/$ID \
  -H "Authorization: Bearer crk_..." \
  -H "Content-Type: application/json" \
  -d '{"config":{"agentFilter":{"mode":"include","keys":["12110","12117"]}}}'

ถ้าเป็น bucket ให้ใช้ config.objectFilter แทน — merge แบบเดียวกัน และไม่แตะ credential เหมือนกัน:

curl -X PATCH https://phone.mcloud.co.th/api/v1/providers/$ID \
  -H "Authorization: Bearer crk_..." \
  -H "Content-Type: application/json" \
  -d '{"config":{"objectFilter":{"mode":"include","folders":["sales"],"minDurationSec":10}}}'
{ "ok": true }

การ merge เป็นราย key ไม่ merge ลึกลงไปข้างใน: การส่ง objectFilter (หรือ agentFilter) คือการแทนที่เงื่อนไขทั้งก้อน ไม่ใช่แก้ทีละฟิลด์ ให้ส่งเงื่อนไขที่ต้องการมาให้ครบ และถ้าจะล้างเงื่อนไขให้ส่ง {"mode":"all"}

DELETE /providers/{id}

ลบ config ของ provider — เรียกซ้ำได้ (idempotent) id ที่ถูกลบไปแล้วก็ยังตอบ ok

Scopes: manage_provider_config

ErrorStatusเมื่อไหร่
provider_kind_not_supported422มีแถวนี้อยู่จริง แต่ไม่ใช่แหล่งดึงสาย (เป็น config ของโมเดล AI)
{ "ok": true }

GET /providers/{id}/people

ดึงรายชื่อ agent/คนที่เลือกได้ของ provider นี้ เพื่อหา key ที่ใช้ตั้งเงื่อนไขการเก็บสาย (config.agentFilter.keys) เป็นการ เรียกสดไปที่ provider (directory extension ของ 3CX / รายชื่อ user ของ DTAC) จึงอาจใช้เวลาสองสามวินาที

ใช้ได้เฉพาะชนิดโทรศัพท์minio เป็นที่เก็บไฟล์ ไม่ใช่ตู้สาขา ไม่มีบัญชีให้ล็อกอินและไม่มีรายชื่อให้อ่าน อีกทั้งเงื่อนไขของมันเลือกที่ "โฟลเดอร์" ไม่ใช่ "คน" จึงตอบ provider_kind_not_supported (422) โดยเจตนา ให้ไปดูรายการไฟล์ในถังจากหน้า Business → Settings → Providers ในแดชบอร์ด แล้วนำ path ที่เห็นมาใส่ใน config.objectFilter.folders

Scopes: manage_provider_config

Fieldความหมาย
keyค่าที่ใส่ใน agentFilter.keys — extension (3CX) / user id (DTAC)
nameชื่อที่แสดง
detailextension (3CX) หรือเบอร์โทร (DTAC)
activeagent เปิดใช้งานอยู่ที่ provider หรือไม่
ErrorStatusเมื่อไหร่
provider_not_found404ไม่มี provider นี้ใน business นี้
provider_kind_not_supported422kind ที่เก็บไว้ไม่ใช่ provider โทรศัพท์
provider_people_unavailable502login/ดึงรายชื่อจาก provider ล้มเหลว (credential ผิด หรือ provider ล่ม)
curl https://phone.mcloud.co.th/api/v1/providers/$ID/people \
  -H "Authorization: Bearer crk_..."
{
  "provider": "threecx",
  "people": [
    { "key": "12110", "name": "Jintana Promdee", "detail": "ext 12110", "active": true }
  ]
}

POST /providers/{id}/test

ทดสอบการเชื่อมต่อด้วย credential ที่เก็บไว้ — คืน 200 ทั้งสองกรณี การเชื่อมต่อล้มเหลวจะแจ้งใน body ไม่ใช่เป็น HTTP error

สำหรับ minio การทดสอบคือการยิง bucketExists แบบเซ็นลายเซ็น 1 ครั้ง จึงตอบได้ทั้ง "credential ถูกไหม" และ "ถังนี้มองเห็นได้ไหม" ในครั้งเดียว — ถ้าพิมพ์ชื่อ bucket ผิดจะได้ minio_bucket_not_found ไม่ใช่ error auth กว้าง ๆ

Scopes: manage_provider_config

curl -X POST https://phone.mcloud.co.th/api/v1/providers/$ID/test \
  -H "Authorization: Bearer crk_..."
{ "ok": true }
{ "ok": false, "error": "authentication failed" }
เอกสาร API