Get results by polling
Polling means your system asks Gowajee for new results on a timer, for example every 2 minutes. It is the recommended way to collect results.
| Polling (recommended) | Webhook | |
|---|---|---|
| How you get results | You call the API on a timer | Gowajee POSTs each result to your server |
| Missed results | None, as long as you read before the data is gone. Your data-retention policy clears old data. The calls of a deleted campaign, or of a deleted recipient, stop showing. | Possible: if your server is down, delivery stops after 3 tries within seconds. |
| What you need | Outbound HTTPS only. Works behind a firewall. | A public HTTPS endpoint |
| Delay | Your polling interval (e.g. 1–2 minutes) | Seconds |
You can also use both: the webhook for speed, and polling as a safety net.
How it works
Section titled “How it works”-
Check progress with
GET /api/campaign/:id/status. It returns the campaign status and how many people are in each state. It is cheap, so use it to decide whether there’s anything new. -
Read new results with
GET /api/campaign/:id/calls. Calls come most recently updated first, 100 per page. Read page 1, 2, 3… and stop as soon as you reach calls that haven’t changed since your last run. -
Save each call with an upsert keyed on
call.id(notsessionId, which isnulluntil a call is dialled). A call can show up again when it changes, for example fromANALYZINGtoCOMPLETED. The upsert simply overwrites it. -
Stop polling only when both are true: the campaign is
COMPLETED,CANCELLEDorEXPIRED, and no call is stillSCHEDULED,QUEUED,INITIATED,RINGING,IN_PROGRESSorANALYZING. A cancelled or expired campaign can still have calls that are finishing. Until then, keep reading.
Which calls are finished?
Section titled “Which calls are finished?”callStatus |
Finished? | What to do |
|---|---|---|
COMPLETED |
✅ | callResult is ready. |
NO_ANSWER, BUSY, FAILED |
✅ for this attempt | A retry may come later as a new call with a new id. |
CANCELLED, EXPIRED |
✅ | No result will come. |
ANALYSIS_FAILED |
✅ for now | No result yet. It can still become COMPLETED later, so keep the call in your upsert. |
SCHEDULED, QUEUED, INITIATED, RINGING, IN_PROGRESS, ANALYZING |
⏳ | Still going. Read it again next time. |
Example
Section titled “Example”This reads only what changed since the last run, using updatedAt as a marker (a “watermark”). Run it every 1–2
minutes per active campaign. Once the campaign is finished, it reads everything each time, and stops when no call is
still going.
/calls lists EXPIRED calls, and calls whose data was deleted by the retention policy, after all the others. So
a read that stops at the watermark can skip them. The full read at the end picks them up.
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"))Good practice
Section titled “Good practice”- Poll each active campaign every 1–2 minutes. There is no fixed rate limit, but faster than every 30 seconds doesn’t get you results any sooner.
- Keep the watermark (
since) in your database, so a restart continues where it stopped. - Want one row per person instead of per call? Read
/recipients. It gives each person’slatestCallStatusandcallAttempt. - Need the recording? Call
/record-urlfor that call when you need it. The link expires within 1 hour.