ข้ามไปยังเนื้อหา

ดึงผลด้วย polling

Polling คือให้ระบบของคุณเรียกถามผลใหม่จาก Gowajee เป็นรอบๆ เช่น ทุก 2 นาที เป็นวิธีเก็บผลที่เราแนะนำ

Polling (แนะนำ) Webhook
ได้ผลมาอย่างไร ระบบของคุณเรียก API ตามรอบเวลา Gowajee ส่ง POST ผลแต่ละรายการมาที่ server ของคุณ
ผลตกหล่น ไม่มี ถ้าอ่านทันก่อนข้อมูลหายไป นโยบายเก็บข้อมูล (data retention) ขององค์กรจะลบข้อมูลเก่าออก และ call ของ campaign หรือ recipient ที่ลบไปแล้วจะไม่แสดงอีก เกิดได้ ถ้า server ล่ม ระบบจะลองส่ง 3 ครั้งภายในไม่กี่วินาที แล้วหยุด
สิ่งที่ต้องมี แค่เรียก HTTPS ออกไปได้ ใช้หลัง firewall ได้ HTTPS endpoint ที่เข้าถึงได้จากภายนอก
ความเร็ว ตามรอบ polling ที่ตั้งไว้ (เช่น 1–2 นาที) ไม่กี่วินาที

ใช้ทั้งสองแบบพร้อมกันก็ได้ ให้ webhook ช่วยให้ได้ผลเร็ว แล้วใช้ polling กันผลตกหล่น

  1. เช็กความคืบหน้า ด้วย GET /api/campaign/:id/status endpoint นี้ตอบ status ของ campaign และจำนวนคนในแต่ละ status กลับมา เรียกได้บ่อยโดยไม่หนักระบบ จึงเหมาะใช้ดูว่ามีอะไรใหม่หรือยัง

  2. อ่านผลใหม่ ด้วย GET /api/campaign/:id/calls ระบบเรียง call ตามเวลาอัปเดตล่าสุด ก่อน page ละ 100 รายการ อ่าน page 1, 2, 3… ไปเรื่อยๆ แล้วหยุดทันทีที่เจอ call ที่ไม่ได้เปลี่ยนเลยตั้งแต่รอบก่อน

  3. บันทึก call แต่ละรายการ แบบ upsert โดยใช้ call.id เป็น key (อย่าใช้ sessionId เพราะจะเป็น null จนกว่า ระบบจะเริ่มโทร call นั้น) call เดิมอาจกลับมาอีกเมื่อข้อมูลเปลี่ยน เช่น เปลี่ยนจาก ANALYZING เป็น COMPLETED การ upsert จะเขียนทับข้อมูลเดิมให้เอง

  4. หยุด polling ได้เมื่อครบทั้งสองข้อเท่านั้น คือ campaign เป็น COMPLETED, CANCELLED หรือ EXPIRED แล้ว และ ไม่มี call ไหนที่ยังเป็น SCHEDULED, QUEUED, INITIATED, RINGING, IN_PROGRESS หรือ ANALYZING เพราะ campaign ที่ยกเลิกหรือหมดเวลาแล้วอาจยังมี call ที่กำลังจะจบอยู่ ระหว่างนั้นให้อ่านต่อไปเรื่อยๆ

callStatus จบแล้วหรือยัง ต้องทำอะไร
COMPLETED ✅ callResult พร้อมใช้แล้ว
NO_ANSWER, BUSY, FAILED ✅ สำหรับรอบนี้ อาจมี retry ตามมาทีหลังเป็น call ใหม่ ที่มี id ใหม่
CANCELLED, EXPIRED ✅ จะไม่มีผลส่งมา
ANALYSIS_FAILED ✅ ในตอนนี้ ยังไม่มีผล แต่ภายหลังอาจเปลี่ยนเป็น COMPLETED ได้ จึงควร upsert call นี้ต่อไปตามปกติ
SCHEDULED, QUEUED, INITIATED, RINGING, IN_PROGRESS, ANALYZING ⏳ ยังไม่จบ อ่านใหม่อีกทีในรอบถัดไป

โค้ดนี้อ่านเฉพาะ call ที่เปลี่ยนไปตั้งแต่รอบก่อน โดยใช้ updatedAt เป็นจุดอ้างอิง (เรียกว่า “watermark”) ให้รันทุก 1–2 นาทีสำหรับแต่ละ campaign ที่ยังทำงานอยู่ พอ campaign จบแล้ว โค้ดจะอ่านทั้งหมดทุกรอบ และหยุดเมื่อไม่เหลือ call ที่ยังไม่จบ

/calls แสดง call ที่เป็น EXPIRED และ call ที่ข้อมูลโดนลบตามนโยบายเก็บข้อมูลไว้ต่อท้าย call อื่นทั้งหมด การอ่านที่หยุดตรง watermark จึงอาจข้าม call กลุ่มนี้ไป แต่การอ่านทั้งหมดในช่วงท้ายจะเก็บมาได้ครบ

const API = 'https://api.voice-agent.gowajee.ai';
const headers = { 'X-API-Key': process.env.GOWAJEE_API_KEY };
const FINAL_CAMPAIGN = ['COMPLETED', 'CANCELLED', 'EXPIRED'];
const UNFINISHED_CALL = ['SCHEDULED', 'QUEUED', 'INITIATED', 'RINGING', 'IN_PROGRESS', 'ANALYZING'];
async function get(path) {
const res = await fetch(`${API}${path}`, { headers });
if (!res.ok) throw new Error(`${res.status} ${path}: ${await res.text()}`);
return res.json();
}
/**
* Pull every call that changed since `since` (a Date) and save it.
* Returns the new watermark, and how many of the calls it read are still going.
*/
async function syncCampaign(campaignId, since) {
// Re-read a small overlap so a call updated during the last run is not missed.
const cutoff = new Date(since.getTime() - 5 * 60 * 1000);
const next = new Date();
let unfinished = 0;
for (let page = 1; ; page++) {
const { data } = await get(`/api/campaign/${campaignId}/calls?page=${page}`);
for (const call of data.calls) {
if (new Date(call.updatedAt) < cutoff) return { next, unfinished }; // everything older is already saved
await saveCall(call); // your upsert, keyed on call.id
if (UNFINISHED_CALL.includes(call.callStatus)) unfinished++;
}
if (page >= data.totalPages) return { next, unfinished };
}
}
async function pollCampaign(campaignId) {
let since = new Date(0); // first run reads everything
for (;;) {
const { data } = await get(`/api/campaign/${campaignId}/status`);
const finished = FINAL_CAMPAIGN.includes(data.status);
// Once the campaign is finished, read everything, and stop only when no call is still going.
const result = await syncCampaign(campaignId, finished ? new Date(0) : since);
since = result.next;
if (finished && result.unfinished === 0) return;
await new Promise((r) => setTimeout(r, 2 * 60 * 1000)); // every 2 minutes
}
}
async function saveCall(call) {
// Example: upsert into your DB. callDuration is a string like "120.120", so parse it.
const duration = call.callDuration == null ? null : parseFloat(call.callDuration);
const fields = call.callResult?.postCallAnalyticsResult ?? call.callResult?.customizableAttr ?? {};
console.log(call.id, call.callStatus, call.recipient?.tel, duration, fields.promise_to_pay?.value);
}
  • Poll ทุก campaign ที่ยังทำงานอยู่ ทุก 1–2 นาที ระบบไม่มี rate limit ตายตัว แต่ถี่กว่าทุก 30 วินาทีก็ไม่ได้ผลเร็วขึ้น
  • เก็บ watermark (since) ไว้ในฐานข้อมูล เวลา restart ระบบจะได้ทำต่อจากจุดเดิม
  • อยากได้ข้อมูล 1 แถวต่อคน แทน 1 แถวต่อ call ให้อ่าน /recipients ซึ่งให้ latestCallStatus และ callAttempt ของแต่ละคน
  • ต้องการไฟล์บันทึกเสียง ให้เรียก /record-url ของ call นั้นตอนที่ต้องใช้ ลิงก์จะหมดอายุ ภายใน 1 ชั่วโมง