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
| Param | Type | Note |
|---|---|---|
kind | "threecx" | "dtac" | "minio" | required |
name | string (2-60) | required |
config | object | required — รูปร่างขึ้นกับ kind |
isActive | boolean | optional (default true) |
config สำหรับ threecx:
| Field | Type | Note |
|---|---|---|
baseUrl | url | required |
username | string | required |
password | string | required (write-only) |
headless | boolean | optional |
config สำหรับ dtac:
| Field | Type | Note |
|---|---|---|
baseUrl | url | optional |
username | string | required |
password | string | required (write-only) |
pollIntervalMinutes | int 5-1440 | optional |
config สำหรับ minio:
bucket แบบ S3 (MinIO, AWS S3, Wasabi, …) ที่ตู้สาขาของคุณทิ้งไฟล์เสียงลงไปอยู่แล้ว — เราอ่านอย่างเดียว ไม่เขียนกลับ
| Field | Type | Note |
|---|---|---|
endpoint | string | required — ใส่เป็น host[:port] หรือ URL เต็ม http(s):// ก็ได้ ถ้ามี scheme จะใช้ scheme ตัดสินว่าใช้ TLS หรือไม่ |
accessKey | string | required |
secretKey | string | required (write-only) |
bucket | string | required |
useSSL | boolean | optional (default false) — ใช้เฉพาะตอน endpoint ไม่มี scheme |
prefix | string | optional — โฟลเดอร์ที่จะไล่ดู เช่น recordings/2026/ เว้นว่าง = ทั้ง bucket |
region | string | optional |
extensions | string[] | optional — นามสกุลไฟล์เสียงที่จะดึง ตัวพิมพ์เล็กและไม่ต้องมีจุด ไม่ใส่ = ใช้รายการเสียงทั้งหมด (wav, mp3, m4a, ogg, opus, flac, …) เพราะ bucket ที่ตู้สาขาเขียนลงมักมี CDR csv/json และไฟล์ .tmp ปนอยู่ด้วย จึงต้องเป็น allowlist ไม่ใช่ blocklist |
direction | "in" | "out" | optional (default "in") — bucket ไม่มีข้อมูลทิศทางสาย จึงต้องประกาศเองว่าโฟลเดอร์นี้เก็บสายแบบไหน แล้วค่านี้จะถูกประทับลงทุกสายที่ดึงเข้ามา |
lookbackHours | int 1-720 | optional (default 48) — พิจารณาเฉพาะไฟล์ที่แก้ไขภายในช่วงย้อนหลังนี้ bucket ที่เก็บประวัติหลายปีจึงไม่ถูกไล่ใหม่ทั้งก้อนทุกรอบ |
pollIntervalMinutes | int 5-1440 | optional (default 15) |
maxObjectsPerTick | int 1-5000 | optional (default 500) — เพดานกันงานบานในรอบเดียว |
เงื่อนไขการเก็บสาย — config.agentFilter (เฉพาะชนิดโทรศัพท์, optional):
เก็บเฉพาะสายของ agent ที่เลือก ทำงาน ก่อนดาวน์โหลด สายที่ถูกกรองออกจะไม่ถูก transcode วิเคราะห์ หรือคิดเงิน ไม่ใส่ (หรือใช้ mode: "all") = เก็บทุกสาย ดูค่า keys ที่ใช้ได้จาก GET /providers/{id}/people
| Field | Type | Note |
|---|---|---|
mode | "all" | "include" | "exclude" | default "all" |
keys | string[] | agent ที่จะเก็บ/ยกเว้น — 3CX: extension · DTAC: user id |
directions | ("in" | "out")[] | optional — ไม่ใส่ = ทั้งสองทิศ |
minDurationSec | int 0-3600 | optional — ตัดสายที่สั้นกว่านี้ |
maxDurationSec | int 0-28800 | optional — ตัดสายที่ยาวกว่านี้ ถ้าใส่ค่าน้อยกว่า minDurationSec ระบบจะไม่ใช้ค่านี้ (ไม่งั้นจะไม่เหลือสายเลย) |
เงื่อนไขการเก็บสาย — config.objectFilter (เฉพาะ minio, optional):
bucket ไม่มีรายชื่อ agent สิ่งที่ตั้งเงื่อนไขได้จริงจึงมีสองอย่าง: ไฟล์ อยู่ที่ไหน และเสียง ยาวเท่าไร สองส่วนนี้บังคับใช้คนละจุดและมีผลกับค่าใช้จ่ายต่างกัน — folders ถูกคัดตอน LIST ของ S3 คือก่อนโหลดแม้แต่ไบต์เดียว ส่วนความยาวไม่มีอยู่ในผลลิสต์ จึงตรวจได้หลังโหลดไฟล์มา probe เท่านั้น เสียค่าโหลด 1 ครั้ง แต่ยังข้ามการเก็บไฟล์ การสร้างแถวสาย การ transcode STT และงาน AI ทุกขั้น
| Field | Type | Note |
|---|---|---|
mode | "all" | "include" | "exclude" | default "all" |
folders | string[] | โฟลเดอร์ที่จะเก็บ/ยกเว้น — ใส่เป็น path เต็ม (pbx/2026/08), path ที่เทียบจาก prefix (08 เมื่อ prefix คือ pbx/2026/) หรือชื่อโฟลเดอร์เปล่า ๆ (sales) ซึ่งจะแมตช์ชั้นนั้นที่ความลึกใดก็ได้ |
minDurationSec | int 0-28800 | optional — ตัดเสียงที่สั้นกว่านี้ |
maxDurationSec | int 0-28800 | optional — ตัดเสียงที่ยาวกว่านี้ ถ้าใส่ค่าน้อยกว่า 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
| Error | Status | เมื่อไหร่ |
|---|---|---|
provider_not_found | 404 | ไม่มี provider นี้ใน business นี้ |
provider_kind_not_supported | 422 | kind ที่เก็บไว้ไม่ใช่แหล่งดึงสาย (เช่นเป็นแถวของโมเดล 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
| Param | Type | Note |
|---|---|---|
name | string (2-60) | optional |
config | object | optional — merge เข้ากับ config ที่เก็บไว้ |
isActive | boolean | optional |
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
| Error | Status | เมื่อไหร่ |
|---|---|---|
provider_kind_not_supported | 422 | มีแถวนี้อยู่จริง แต่ไม่ใช่แหล่งดึงสาย (เป็น 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 | ชื่อที่แสดง |
detail | extension (3CX) หรือเบอร์โทร (DTAC) |
active | agent เปิดใช้งานอยู่ที่ provider หรือไม่ |
| Error | Status | เมื่อไหร่ |
|---|---|---|
provider_not_found | 404 | ไม่มี provider นี้ใน business นี้ |
provider_kind_not_supported | 422 | kind ที่เก็บไว้ไม่ใช่ provider โทรศัพท์ |
provider_people_unavailable | 502 | login/ดึงรายชื่อจาก 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" }
