Phones

ติดต่อครั้งแรก · สรุปสายรายเบอร์ · หลายพันเบอร์ต่อคำขอ

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):

  1. เก็บเฉพาะตัวเลข
  2. ขึ้นต้นด้วย 66 และยาวเกิน 9 หลัก → ตัด 66 ออก
  3. ขึ้นต้นด้วย 0 และยาว 10 หลักพอดี → ตัด 0 ออก
  4. นอกนั้น ถ้ายาวเกิน 9 หลัก → เก็บ 9 หลักท้าย
  5. นอกนั้น (9 หลักหรือน้อยกว่า) → ใช้ตามเดิม
ที่ส่งPhone key
0812345678812345678
+66 81-234-5678812345678
66812345678812345678
812345678812345678
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, null422 invalid_payload
คีย์ที่ endpoint ไม่รู้จัก ไม่ว่าอยู่ชั้นไหน (สะกดผิดอย่าง capturedat, customerPhone5)400 validation_failed — ระบบไม่ข้ามคีย์เหล่านี้ ตัวกรองที่สะกดผิดจึงไม่ทำให้ได้ผลกว้างกว่าที่ตั้งใจโดยไม่รู้ตัว
ค่าที่ผิดชนิดหรือเกินขอบเขต400 validation_failed
null ในฟิลด์ค่าเดี่ยว (tz, dateFrom, direction, limit, …)ถือว่าไม่ได้ตั้ง — เหมือนไม่ส่งคีย์นั้น
ตัวกรองแบบรายการที่ไม่ส่งมาไม่กรอง
ตัวกรองแบบรายการที่ส่งเป็น []ไม่ตรงกับสายไหนเลย
ตัวกรองแบบรายการที่ส่งเป็น null400 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

ฟิลด์TypeNote
pairsarray 1–5,000 รายการrequired — ค้นทีละคู่ และตอบกลับตามลำดับเดิม
pairs[].keystring 1–100 ตัวอักษรrequired — id ของลีดฝั่งคุณ (เช่น LOAN:123) ส่งกลับไปตรงตามที่ส่งมาทุกตัวอักษร (ไม่ตัดช่องว่าง) · ซ้ำกันได้ แต่ละคู่ถูกตอบแยกกัน
pairs[].phonestring ไม่เกิน 100 ตัวอักษรrequired — รูปแบบไหนก็ได้ จับคู่ด้วย phone key (ดู วิธีจับคู่เบอร์โทร) · ค่าที่ไม่มีตัวเลขจะได้ found: false
pairs[].capturedAtวันที่หรือ ISO 8601required — เวลาที่เก็บลีดมา ถ้าไม่ระบุ timezone อ่านตาม tz · วันล้วนหมายถึง 00:00 ของวันนั้น
tzIANA timezoneoptional, default Asia/Bangkok

ระบบตอบแต่ละคู่แบบนี้:

  1. สายแรกสุดของเบอร์นั้นที่เกิดตอนหรือหลัง capturedAt ทั้งโทรเข้าและโทรออก → before: false
  2. ถ้าไม่มี ใช้สายล่าสุดที่เกิดก่อน capturedAt → before: true คือลูกค้าเคยโทรมาหรือถูกโทรหาก่อนที่คุณจะเก็บลีด — ให้ถือว่าช่วงเวลารอเป็น 0
  3. ไม่มีสายของเบอร์นั้นเลย → 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 }
  ]
}
ฟิลด์ความหมาย
keykey ของคุณ ตรงตามที่ส่งมา
foundเจอสายของเบอร์นั้นหรือไม่
callIdสายที่เจอ — อ่านต่อด้วย GET /recordings/{id}
callAtเวลาที่โทร เป็น UTC
directionin | out
agentKeyพนักงานของสายนั้น เป็น key เดียวกับที่ GET /recordings/agents คืน และที่ตัวกรอง agentKey ทุกตัวรับ (สายที่เชื่อมมาจากธุรกิจอื่นอาจเป็น <businessId>:<key>) · null เมื่อสายไม่ระบุพนักงาน
agentNamecallcenterName ที่บันทึกไว้กับสาย
beforefalse = สายแรกตอนหรือหลัง capturedAt · true = ไม่มีสายหลังจากนั้น นี่คือสายล่าสุดก่อนหน้า

POST /phones/summary

สรุปสายของหลายเบอร์ในคำขอเดียว: แต่ละเบอร์มีกี่สาย เข้า/ออกเท่าไร ไม่ได้รับกี่สาย เวลาคุย คะแนนเฉลี่ย สายแรกและสายล่าสุด และ — ถ้าขอ — id ของสายล่าสุด ได้ 1 รายการต่อ 1 เบอร์ รวมเบอร์ที่ไม่มีสายด้วย

แพ็กเกจ: ถ้าแพ็กเกจไม่รวมการส่งผลการวิเคราะห์ออก avgScore ของทุกรายการเป็น null ส่วนตัวเลขจำนวนยังครบ ดู เริ่มต้นใช้งาน → Endpoint ที่ขึ้นกับแพ็กเกจ

Scopes: view_recording_all · view_recording_self

ฟิลด์TypeNote
phonesstring[] 1–10,000 ค่า (แต่ละค่าไม่เกิน 100 ตัวอักษร)required — รูปแบบไหนก็ได้ จับคู่ด้วย phone key · เบอร์เดียวกันต่างรูปแบบนับครั้งเดียว
callDateFromวันที่หรือ ISO 8601optional — ดู Recordings → วันที่และเวลา
callDateToวันที่หรือ ISO 8601optional — ค่าที่เป็นวันล้วนหมายถึงสิ้นวัน (รวมวันนั้น)
tzIANA timezoneoptional, default Asia/Bangkok
processingStatusarray ของ pending | transcribing | completed | failed | insufficient_creditoptional — เฉพาะสายที่อยู่ในสถานะใดสถานะหนึ่งนี้ · ไม่ส่ง = ทุกสถานะ · [] = ไม่มีสายไหนเลย ทุกเบอร์จึงได้ค่าเป็นศูนย์
directionin | outoptional
lastCallsint 0–5optional, 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 ด้านบนเป็นมือถือเบอร์เดียวกัน จึงได้รายการเดียว

ฟิลด์ความหมาย
phoneKeyphone 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
lastCallIdsid ของสายล่าสุดไม่เกิน lastCalls สาย ใหม่สุดก่อน · เป็น [] เมื่อ lastCalls เป็น 0

ตัวเลขชุดนี้ใช้กติกาแบบ CRM ไม่ใช่กติกาเดียวกับตัวเลขในแอป — อ่านให้ถูก:

  • avgScore รวมสายสั้นด้วย ตัวเลขคะแนนในแอป, GET /recordings/stats และ /insights/* ตัดสายที่แท็ก short ออกจากค่าเฉลี่ยคะแนนทุกตัว สำหรับสายชุดเดียวกัน ตัวเลขนี้จึงอาจไม่เท่ากับของที่อื่น
  • missed ซ้อนกับ inbound / outbound — ไม่ใช่ทิศทางที่สาม และไม่ใช่ missed ของ /insights/* ซึ่งหมายถึงคำตัดสิน no_answer ของ AI
  • totalDurationSec อาจต่างจาก totalDurationMs / 1000 ได้สูงสุดครึ่งวินาทีต่อสาย เพราะแต่ละสายถูกปัดก่อนรวม
  • เบอร์ที่ไม่มีสายในช่วงนั้นยังได้รายการของตัวเอง — ค่าเป็นศูนย์, null และ [] — ไม่ถูกตัดทิ้ง · ถ้าไม่มีค่าไหนใน phones มีตัวเลขเลย items จะเป็น []

ข้อผิดพลาด

codeHTTPเมื่อไหร่
invalid_payload422body ไม่ใช่ JSON object
validation_failed400body ไม่ตรงตามที่กำหนด — ดู Body ของคำขอแบบ batch
insufficient_scope403API key ไม่มีทั้ง view_recording_all และ view_recording_self

account key ยังได้ 400 business_required และ 403 forbidden_business ตามกติกาเดียวกับ endpoint อื่น