ดึงผลด้วย 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 กันผลตกหล่น
ทำงานอย่างไร
หัวข้อที่มีชื่อว่า “ทำงานอย่างไร”-
เช็กความคืบหน้า ด้วย
GET /api/campaign/:id/statusendpoint นี้ตอบ status ของ campaign และจำนวนคนในแต่ละ status กลับมา เรียกได้บ่อยโดยไม่หนักระบบ จึงเหมาะใช้ดูว่ามีอะไรใหม่หรือยัง -
อ่านผลใหม่ ด้วย
GET /api/campaign/:id/callsระบบเรียง call ตามเวลาอัปเดตล่าสุด ก่อน page ละ 100 รายการ อ่าน page 1, 2, 3… ไปเรื่อยๆ แล้วหยุดทันทีที่เจอ call ที่ไม่ได้เปลี่ยนเลยตั้งแต่รอบก่อน -
บันทึก call แต่ละรายการ แบบ upsert โดยใช้
call.idเป็น key (อย่าใช้sessionIdเพราะจะเป็นnullจนกว่า ระบบจะเริ่มโทร call นั้น) call เดิมอาจกลับมาอีกเมื่อข้อมูลเปลี่ยน เช่น เปลี่ยนจากANALYZINGเป็นCOMPLETEDการ upsert จะเขียนทับข้อมูลเดิมให้เอง -
หยุด polling ได้เมื่อครบทั้งสองข้อเท่านั้น คือ campaign เป็น
COMPLETED,CANCELLEDหรือEXPIREDแล้ว และ ไม่มี call ไหนที่ยังเป็นSCHEDULED,QUEUED,INITIATED,RINGING,IN_PROGRESSหรือANALYZINGเพราะ campaign ที่ยกเลิกหรือหมดเวลาแล้วอาจยังมี call ที่กำลังจะจบอยู่ ระหว่างนั้นให้อ่านต่อไปเรื่อยๆ
call ไหนจบแล้วบ้าง
หัวข้อที่มีชื่อว่า “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);}import osimport timefrom datetime import datetime, timedelta, timezone
import requests
API = "https://api.voice-agent.gowajee.ai"HEADERS = {"X-API-Key": os.environ["GOWAJEE_API_KEY"]}FINAL_CAMPAIGN = {"COMPLETED", "CANCELLED", "EXPIRED"}UNFINISHED_CALL = {"SCHEDULED", "QUEUED", "INITIATED", "RINGING", "IN_PROGRESS", "ANALYZING"}EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
def get(path: str) -> dict: res = requests.get(f"{API}{path}", headers=HEADERS, timeout=30) res.raise_for_status() return res.json()
def parse_ts(value: str) -> datetime: return datetime.fromisoformat(value.replace("Z", "+00:00"))
def sync_campaign(campaign_id: str, since: datetime) -> tuple[datetime, int]: """Save every call that changed since `since`. Returns the next watermark, and how many of the calls it read are still going.""" cutoff = since - timedelta(minutes=5) # small overlap, so nothing is missed next_since = datetime.now(timezone.utc) unfinished = 0
page = 1 while True: data = get(f"/api/campaign/{campaign_id}/calls?page={page}")["data"] for call in data["calls"]: if parse_ts(call["updatedAt"]) < cutoff: return next_since, unfinished # everything older is already saved save_call(call) # your upsert, keyed on call["id"] if call["callStatus"] in UNFINISHED_CALL: unfinished += 1 if page >= data["totalPages"]: return next_since, unfinished page += 1
def poll_campaign(campaign_id: str) -> None: since = EPOCH # first run reads everything while True: status = get(f"/api/campaign/{campaign_id}/status")["data"]["status"] finished = status in FINAL_CAMPAIGN
# Once the campaign is finished, read everything, and stop only when no call is still going. since, unfinished = sync_campaign(campaign_id, EPOCH if finished else since) if finished and unfinished == 0: return time.sleep(120) # every 2 minutes
def save_call(call: dict) -> None: # callDuration is a string like "120.120", so parse it. duration = float(call["callDuration"]) if call.get("callDuration") is not None else None result = call.get("callResult") or {} fields = result.get("postCallAnalyticsResult") or result.get("customizableAttr") or {} tel = (call.get("recipient") or {}).get("tel") print(call["id"], call["callStatus"], tel, duration, fields.get("promise_to_pay", {}).get("value"))แนวทางที่ควรทำ
หัวข้อที่มีชื่อว่า “แนวทางที่ควรทำ”- Poll ทุก campaign ที่ยังทำงานอยู่ ทุก 1–2 นาที ระบบไม่มี rate limit ตายตัว แต่ถี่กว่าทุก 30 วินาทีก็ไม่ได้ผลเร็วขึ้น
- เก็บ watermark (
since) ไว้ในฐานข้อมูล เวลา restart ระบบจะได้ทำต่อจากจุดเดิม - อยากได้ข้อมูล 1 แถวต่อคน แทน 1 แถวต่อ call ให้อ่าน
/recipientsซึ่งให้latestCallStatusและcallAttemptของแต่ละคน - ต้องการไฟล์บันทึกเสียง ให้เรียก
/record-urlของ call นั้นตอนที่ต้องใช้ ลิงก์จะหมดอายุ ภายใน 1 ชั่วโมง