Phones
ตอบเป็นรายเบอร์โทร สำหรับระบบที่มีฐานลูกค้าของตัวเอง — เช่น CRM ที่อยากรู้ว่า "เบอร์ลูกค้าของเราแต่ละเบอร์มีสายอะไรบ้าง" ทีละหลายพันเบอร์ในคำขอเดียว แทนการไล่เรียก GET /recordings ทีละลูกค้า
Account key: ทุก request ด้านล่างต้องส่ง
X-Business-Id: <business id>เพื่อเลือกธุรกิจที่จะทำงานด้วย (ถ้าไม่ส่งจะได้400 business_required) ส่วน business key จะไม่อ่าน header นี้ ดูเพิ่มที่ เริ่มต้นใช้งาน → API key ระดับบัญชี
ทั้งสอง endpoint ในหน้านี้เป็นการอ่าน ที่ใช้ POST เพียงเพราะเบอร์หลายพันเบอร์ใส่ใน URL ไม่ได้ ข้อมูลที่เห็นคือสายชุดเดียวกับที่ GET /recordings แสดงให้ API key เดียวกันพอดี — รวมสายที่ธุรกิจอื่นเชื่อมเข้ามา (อ่านอย่างเดียว) และ API key ที่มีแค่ view_recording_self จะเห็นเฉพาะสายที่ผูกกับผู้ใช้เจ้าของ key
กติกาสองข้อด้านล่างใช้กับทุก endpoint แบบ batch: สองตัวในหน้านี้, POST /recordings/search และ /recordings/stats, /insights/overview, /insights/agents, /insights/timeseries แบบ POST (ดูหน้า Recordings)
วิธีจับคู่เบอร์โทร
endpoint แบบ batch จับคู่เบอร์ด้วย phone key ไม่ได้เทียบสตริงตรง ๆ ทั้งสองฝั่งผ่านกติกาเดียวกัน — ทั้งเบอร์ที่คุณส่งมา และเบอร์ของแต่ละสาย (customerPhoneNormalized ของสาย หรือ customerPhone ถ้าสายนั้นไม่มีเบอร์แบบ normalized):
- เก็บเฉพาะตัวเลข
- ขึ้นต้นด้วย
66และยาวเกิน 9 หลัก → ตัด66ออก - ขึ้นต้นด้วย
0และยาว 10 หลักพอดี → ตัด0ออก - นอกนั้น ถ้ายาวเกิน 9 หลัก → เก็บ 9 หลักท้าย
- นอกนั้น (9 หลักหรือน้อยกว่า) → ใช้ตามเดิม
| ที่ส่ง | Phone key |
|---|---|
0812345678 | 812345678 |
+66 81-234-5678 | 812345678 |
66812345678 | 812345678 |
812345678 | 812345678 |
021234567 (เบอร์บ้านกรุงเทพฯ) | 021234567 |
- เบอร์มือถือไทยจับคู่ได้ทุกรูปแบบ — จะมีหรือไม่มี
+66,0นำหน้า, ช่องว่าง หรือขีด ก็ได้ ส่งตามที่ระบบของคุณเก็บไว้ได้เลย - เบอร์บ้าน 9 หลักยังคง
0นำหน้าไว้ - เบอร์เดียวกันที่เขียนต่างรูปแบบถือเป็นเบอร์เดียว เพราะได้ key เดียวกัน
- ค่าที่ไม่มีตัวเลขเลยจะไม่เจอสายไหน ไม่ใช่ error
customerPhone/customerPhoneNormalizedของGET /recordings(และcustomerPhoneNormalizedของGET /recordings/stats) ยังเป็นการเทียบตรงตัวเหมือนเดิม มีแต่ endpoint แบบ batch ที่ใช้ phone key
Body ของคำขอแบบ batch
ทุก POST แบบ batch อ่าน JSON body ด้วยกติกาเดียวกัน client ตัวเดียวจึงใช้ได้กับทุกตัว:
| ที่ส่ง | ผล |
|---|---|
body ที่ไม่ใช่ JSON object — ไม่ใช่ JSON เลย, array, string, null | 422 invalid_payload |
คีย์ที่ endpoint ไม่รู้จัก ไม่ว่าอยู่ชั้นไหน (สะกดผิดอย่าง capturedat, customerPhone5) | 400 validation_failed — ระบบไม่ข้ามคีย์เหล่านี้ ตัวกรองที่สะกดผิดจึงไม่ทำให้ได้ผลกว้างกว่าที่ตั้งใจโดยไม่รู้ตัว |
| ค่าที่ผิดชนิดหรือเกินขอบเขต | 400 validation_failed |
null ในฟิลด์ค่าเดี่ยว (tz, dateFrom, direction, limit, …) | ถือว่าไม่ได้ตั้ง — เหมือนไม่ส่งคีย์นั้น |
| ตัวกรองแบบรายการที่ไม่ส่งมา | ไม่กรอง |
ตัวกรองแบบรายการที่ส่งเป็น [] | ไม่ตรงกับสายไหนเลย |
ตัวกรองแบบรายการที่ส่งเป็น null | 400 validation_failed |
ตัวกรองแบบรายการคือ customerPhones, ids, tags, processingStatus, reviewGrade, agentKey, callcenterNames, flag และ flagLevel — เท่าที่แต่ละ endpoint รับ · ค่าในรายการเดียวกัน ตรงค่าใดค่าหนึ่งก็พอ (OR) · ส่วนตัวกรองต่างตัวต้องตรงทุกตัว (AND)
ทำไม [] จึงไม่เจออะไรเลย: ระบบของคุณแปลงตัวกรองของตัวเอง — "ลูกค้าของแคมเปญ X" — เป็นรายการเบอร์ ถ้ารายการนั้นว่าง คำตอบที่ถูกคือศูนย์สาย ไม่ใช่ทุกสาย · ถ้าหมายถึง "ไม่กรอง" ให้ไม่ส่งคีย์นั้นมาเลย (รายการเบอร์ที่ไม่มีตัวเลขสักค่าก็ไม่เจออะไรเช่นกัน)
ส่วนรายการที่เป็นข้อมูลบังคับใช้กติกาต่างออกไป: pairs ของ /phones/first-contact และ phones ของ /phones/summary ต้องมีอย่างน้อย 1 รายการ ทั้ง [] และ null จึงได้ 400 · ฟิลด์บังคับข้างใน (pairs[].phone, …) ก็ปล่อยว่างไม่ได้เช่นกัน — ส่ง null ได้ 400
body ของ validation_failed แจกแจงทุกปัญหาไว้ใน issues: path บอกว่าอยู่ตรงไหน message บอกว่าผิดอะไร · ถ้าเป็นคีย์ที่ไม่รู้จัก path คือ object ที่คีย์นั้นอยู่ (ชั้นบนสุดเป็น "") และ message บอกชื่อคีย์:
{
"error": {
"code": "validation_failed",
"issues": [
{ "path": "filters", "message": "Unrecognized key: \"customerPhone5\"" },
{ "path": "pairs.3.capturedAt", "message": "Expected YYYY-MM-DD or an ISO 8601 date-time (2026-05-01T08:00:00Z / 2026-05-01T08:00:00+07:00). A value with no zone is read in `tz`." }
]
}
}
วันที่ใช้กติกาเดียวกับ Recordings → วันที่และเวลา: ค่าที่บอก timezone มาเองใช้ตามนั้น ค่าที่ไม่บอกอ่านตาม tz (ค่าเริ่มต้น Asia/Bangkok)
POST /phones/first-contact
สำหรับลีดแต่ละรายในระบบของคุณ — เบอร์หนึ่งเบอร์กับเวลาที่เก็บลีดนั้นมา — ตอบว่าสายแรกของเบอร์นั้นที่เกิดตอนหรือหลังเวลานั้นคือสายไหน ไม่ว่าโทรเข้าหรือโทรออก ใช้ตอบคำถาม "ใช้เวลานานแค่ไหนกว่าจะติดต่อลีดนี้ได้ และใครเป็นคนติดต่อ"
Scopes: view_recording_all · view_recording_self
| ฟิลด์ | Type | Note |
|---|---|---|
pairs | array 1–5,000 รายการ | required — ค้นทีละคู่ และตอบกลับตามลำดับเดิม |
pairs[].key | string 1–100 ตัวอักษร | required — id ของลีดฝั่งคุณ (เช่น LOAN:123) ส่งกลับไปตรงตามที่ส่งมาทุกตัวอักษร (ไม่ตัดช่องว่าง) · ซ้ำกันได้ แต่ละคู่ถูกตอบแยกกัน |
pairs[].phone | string ไม่เกิน 100 ตัวอักษร | required — รูปแบบไหนก็ได้ จับคู่ด้วย phone key (ดู วิธีจับคู่เบอร์โทร) · ค่าที่ไม่มีตัวเลขจะได้ found: false |
pairs[].capturedAt | วันที่หรือ ISO 8601 | required — เวลาที่เก็บลีดมา ถ้าไม่ระบุ timezone อ่านตาม tz · วันล้วนหมายถึง 00:00 ของวันนั้น |
tz | IANA timezone | optional, default Asia/Bangkok |
ระบบตอบแต่ละคู่แบบนี้:
- สายแรกสุดของเบอร์นั้นที่เกิดตอนหรือหลัง
capturedAtทั้งโทรเข้าและโทรออก →before: false - ถ้าไม่มี ใช้สายล่าสุดที่เกิดก่อน
capturedAt→before: trueคือลูกค้าเคยโทรมาหรือถูกโทรหาก่อนที่คุณจะเก็บลีด — ให้ถือว่าช่วงเวลารอเป็น 0 - ไม่มีสายของเบอร์นั้นเลย →
found: falseและฟิลด์อื่นเป็นnullทั้งหมด
- ทุกสายนับหมด ไม่ว่า
processingStatusจะเป็นอะไร — สายที่ยังรอวิเคราะห์ หรือวิเคราะห์ไม่สำเร็จ ก็ยังนับเป็นการติดต่อ - สายที่เชื่อมมาจากธุรกิจอื่นนับเหมือนสายของตัวเอง
- ถ้ามีสองสายเวลาตรงกันพอดี ระบบจะตอบสายเดิมทุกครั้ง
curl -X POST https://phone.mcloud.co.th/api/v1/phones/first-contact \
-H "Authorization: Bearer crk_..." \
-H "Content-Type: application/json" \
-d '{
"tz": "Asia/Bangkok",
"pairs": [
{ "key": "LOAN:123", "phone": "081-234-5678", "capturedAt": "2026-10-01T09:30:00" },
{ "key": "LOAN:124", "phone": "+66898765432", "capturedAt": "2026-10-02T14:00:00+07:00" },
{ "key": "LOAN:125", "phone": "0611111111", "capturedAt": "2026-10-03" }
]
}'
{
"items": [
{ "key": "LOAN:123", "found": true, "callId": "0192b9c4-8e71-7a13-9d02-5f1a6c3b7e90", "callAt": "2026-10-01T02:41:07.000Z", "direction": "out", "agentKey": "12110", "agentName": "Somchai", "before": false },
{ "key": "LOAN:124", "found": true, "callId": "0192b9c5-1a04-7b88-8c3d-7e5049216bb2", "callAt": "2026-10-01T03:12:44.000Z", "direction": "in", "agentKey": "12117", "agentName": "Nok", "before": true },
{ "key": "LOAN:125", "found": false, "callId": null, "callAt": null, "direction": null, "agentKey": null, "agentName": null, "before": null }
]
}
| ฟิลด์ | ความหมาย |
|---|---|
key | key ของคุณ ตรงตามที่ส่งมา |
found | เจอสายของเบอร์นั้นหรือไม่ |
callId | สายที่เจอ — อ่านต่อด้วย GET /recordings/{id} |
callAt | เวลาที่โทร เป็น UTC |
direction | in | out |
agentKey | พนักงานของสายนั้น เป็น key เดียวกับที่ GET /recordings/agents คืน และที่ตัวกรอง agentKey ทุกตัวรับ (สายที่เชื่อมมาจากธุรกิจอื่นอาจเป็น <businessId>:<key>) · null เมื่อสายไม่ระบุพนักงาน |
agentName | callcenterName ที่บันทึกไว้กับสาย |
before | false = สายแรกตอนหรือหลัง capturedAt · true = ไม่มีสายหลังจากนั้น นี่คือสายล่าสุดก่อนหน้า |
POST /phones/summary
สรุปสายของหลายเบอร์ในคำขอเดียว: แต่ละเบอร์มีกี่สาย เข้า/ออกเท่าไร ไม่ได้รับกี่สาย เวลาคุย คะแนนเฉลี่ย สายแรกและสายล่าสุด และ — ถ้าขอ — id ของสายล่าสุด ได้ 1 รายการต่อ 1 เบอร์ รวมเบอร์ที่ไม่มีสายด้วย
แพ็กเกจ: ถ้าแพ็กเกจไม่รวมการส่งผลการวิเคราะห์ออก
avgScoreของทุกรายการเป็นnullส่วนตัวเลขจำนวนยังครบ ดู เริ่มต้นใช้งาน → Endpoint ที่ขึ้นกับแพ็กเกจ
Scopes: view_recording_all · view_recording_self
| ฟิลด์ | Type | Note |
|---|---|---|
phones | string[] 1–10,000 ค่า (แต่ละค่าไม่เกิน 100 ตัวอักษร) | required — รูปแบบไหนก็ได้ จับคู่ด้วย phone key · เบอร์เดียวกันต่างรูปแบบนับครั้งเดียว |
callDateFrom | วันที่หรือ ISO 8601 | optional — ดู Recordings → วันที่และเวลา |
callDateTo | วันที่หรือ ISO 8601 | optional — ค่าที่เป็นวันล้วนหมายถึงสิ้นวัน (รวมวันนั้น) |
tz | IANA timezone | optional, default Asia/Bangkok |
processingStatus | array ของ pending | transcribing | completed | failed | insufficient_credit | optional — เฉพาะสายที่อยู่ในสถานะใดสถานะหนึ่งนี้ · ไม่ส่ง = ทุกสถานะ · [] = ไม่มีสายไหนเลย ทุกเบอร์จึงได้ค่าเป็นศูนย์ |
direction | in | out | optional |
lastCalls | int 0–5 | optional, default 0 — ต้องการ id ของสายล่าสุดกี่สายต่อเบอร์ |
curl -X POST https://phone.mcloud.co.th/api/v1/phones/summary \
-H "Authorization: Bearer crk_..." \
-H "Content-Type: application/json" \
-d '{
"phones": ["0812345678", "+66 81-234-5678", "+66 89-876-5432"],
"callDateFrom": "2026-09-01",
"lastCalls": 2
}'
{
"items": [
{
"phoneKey": "812345678",
"total": 7,
"inbound": 4,
"outbound": 3,
"missed": 2,
"totalDurationMs": 1284320,
"totalDurationSec": 1285,
"avgScore": 74.33,
"firstCallAt": "2026-09-02T03:14:00.000Z",
"lastCallAt": "2026-10-14T08:02:51.412Z",
"lastCallIds": ["0192c1d4-7a20-7b61-9e3f-2d81c5a04b17", "0192bf02-55e1-7c08-8a4d-9b3e17f06c22"]
},
{
"phoneKey": "898765432",
"total": 0,
"inbound": 0,
"outbound": 0,
"missed": 0,
"totalDurationMs": 0,
"totalDurationSec": 0,
"avgScore": null,
"firstCallAt": null,
"lastCallAt": null,
"lastCallIds": []
}
]
}
สองค่าแรกของ phones ด้านบนเป็นมือถือเบอร์เดียวกัน จึงได้รายการเดียว
| ฟิลด์ | ความหมาย |
|---|---|
phoneKey | phone key ที่เบอร์ของคุณจับคู่ได้ · รายการเรียงตามลำดับที่ key นั้นปรากฏครั้งแรกใน phones — แปลงเบอร์ของคุณด้วยกติกาด้านบนเพื่อจับคู่กลับ |
total | จำนวนสาย |
inbound / outbound | จำนวนสายเข้า / สายออก |
missed | สายที่ไม่มีความยาว ความยาวปัดแล้วเป็น 0 วินาที หรือมีแท็ก short |
totalDurationMs | ผลรวม durationMs |
totalDurationSec | ผลรวมความยาวของแต่ละสายที่ปัดเป็นวินาทีก่อนแล้วค่อยรวม (ปัดครึ่งขึ้น: 499 ms → 0, 500 ms → 1) |
avgScore | ค่าเฉลี่ยธรรมดาของ score (คะแนนเทเล) ของทุกสายที่มีคะแนน ทศนิยม 2 ตำแหน่ง · null เมื่อไม่มีสายไหนมีคะแนน |
firstCallAt / lastCallAt | สายเก่าสุดและใหม่สุด เป็น UTC |
lastCallIds | id ของสายล่าสุดไม่เกิน lastCalls สาย ใหม่สุดก่อน · เป็น [] เมื่อ lastCalls เป็น 0 |
ตัวเลขชุดนี้ใช้กติกาแบบ CRM ไม่ใช่กติกาเดียวกับตัวเลขในแอป — อ่านให้ถูก:
avgScoreรวมสายสั้นด้วย ตัวเลขคะแนนในแอป,GET /recordings/statsและ/insights/*ตัดสายที่แท็กshortออกจากค่าเฉลี่ยคะแนนทุกตัว สำหรับสายชุดเดียวกัน ตัวเลขนี้จึงอาจไม่เท่ากับของที่อื่นmissedซ้อนกับinbound/outbound— ไม่ใช่ทิศทางที่สาม และไม่ใช่missedของ/insights/*ซึ่งหมายถึงคำตัดสินno_answerของ AItotalDurationSecอาจต่างจากtotalDurationMs / 1000ได้สูงสุดครึ่งวินาทีต่อสาย เพราะแต่ละสายถูกปัดก่อนรวม- เบอร์ที่ไม่มีสายในช่วงนั้นยังได้รายการของตัวเอง — ค่าเป็นศูนย์,
nullและ[]— ไม่ถูกตัดทิ้ง · ถ้าไม่มีค่าไหนในphonesมีตัวเลขเลยitemsจะเป็น[]
ข้อผิดพลาด
code | HTTP | เมื่อไหร่ |
|---|---|---|
invalid_payload | 422 | body ไม่ใช่ JSON object |
validation_failed | 400 | body ไม่ตรงตามที่กำหนด — ดู Body ของคำขอแบบ batch |
insufficient_scope | 403 | API key ไม่มีทั้ง view_recording_all และ view_recording_self |
account key ยังได้ 400 business_required และ 403 forbidden_business ตามกติกาเดียวกับ endpoint อื่น