Part 2C — Calls, call-outs, OTP, reports
← Contents · Previous: Part 2B — Call flows · Next: Part 2D — Agents
All endpoints: 🔒 Protected · Bearer API key. A call or call-out of another client app gives 404.
G. Calls and their reports
Every call (in or out) is one record with a summary, a timeline of what happened, and — when something was recorded —
audio and a transcript. The same summary arrives in the call.ended webhook (Part 3).
G1. List calls
Endpoint: GET {base_url}/calls
| Query | Type | Required | Description | Default |
|---|---|---|---|---|
accountId |
uuid | No | One account (without it: all the client app's calls) | |
from / to |
ISO instant | No | Started between (2026-11-13T00:00:00Z) |
|
type |
string | No | INCOMING · BROADCAST · CAMPAIGN · OTP · AGENT_CALL · TEST |
|
result |
string | No | e.g. MISSED, VOICEMAIL (Part 4) |
|
status |
string | No | e.g. LIVE for calls in progress |
|
number |
string | No | Calls from or to this E.164 number | |
calloutId |
uuid | No | Calls placed by one call-out | |
page / size |
integer | No | Page (from 0) and page size | 0 / 50 |
Success Response JSON Sample (one call shown; each item is a G2 summary):
{
"success": true,
"httpStatus": "200 OK",
"message": "Calls",
"action": null,
"action_time": "2026-11-13T20:05:32.490000000",
"data": {
"calls": [ { "callId": "355760f8-51c0-47c8-b3b0-49c3612e4b96", "type": "INCOMING", "result": "VOICEMAIL", "…": "…" } ],
"page": 0,
"size": 50,
"total": 65
}
}
Newest first. A wrong value for a filter → 400 Invalid value for parameter: <name>.
G2. Get a call
Endpoint: GET {base_url}/calls/{callId}
Success Response JSON Sample (a caller left a voicemail while waiting for an agent):
{
"success": true,
"httpStatus": "200 OK",
"message": "Call",
"action": null,
"action_time": "2026-11-13T20:05:32.497509178",
"data": {
"callId": "355760f8-51c0-47c8-b3b0-49c3612e4b96",
"accountRef": "softphone",
"accountId": "adb72fac-66a2-471b-a437-035fe008197d",
"type": "INCOMING",
"direction": "INBOUND",
"from": "+255745051250",
"to": "+255677000001",
"status": "ENDED",
"result": "VOICEMAIL",
"endReason": "SCRIPT_END",
"endedBy": "SYSTEM",
"handledBy": "VOICEMAIL",
"startedAt": "2026-11-13T16:45:27.909648Z",
"answeredAt": "2026-11-13T16:45:27.909648Z",
"endedAt": "2026-11-13T16:46:20.533012Z",
"durations": { "ringSeconds": null, "totalSeconds": 53, "scriptSeconds": 16, "waitSeconds": 35,
"talkSeconds": 0, "holdSeconds": 0, "aiSeconds": 0 },
"keys": null,
"lastBlock": "vm",
"outcomes": [],
"usage": { "pstnSeconds": 53, "trunkSeconds": 53, "waitSeconds": 35, "aiSeconds": 0, "ttsChars": 0, "sttSeconds": 0 },
"providerCost": {
"tzs": 25.61,
"usd": 0.000893,
"items": [
{ "provider": "AT", "tzs": 25.61, "usd": 0.0, "lines": [
{ "item": "AT_INBOUND", "qty": 1, "unit": "MINUTE", "rate": 17, "currency": "TZS", "amount": 17, "source": "RATE", "model": null },
{ "item": "AT_SIP_TRUNK", "qty": 1, "unit": "MINUTE", "rate": 8.61, "currency": "TZS", "amount": 8.61, "source": "RATE", "model": null } ] },
{ "provider": "OPENAI", "tzs": 0.0, "usd": 0.000893, "lines": [
{ "item": "OPENAI_CHAT_IN", "qty": 474, "unit": "TOKEN", "rate": 1e-07, "currency": "USD", "amount": 4.74e-05, "source": "OPENAI", "model": "gpt-6-luna" },
{ "item": "OPENAI_CHAT_OUT", "qty": 42, "unit": "TOKEN", "rate": 5e-07, "currency": "USD", "amount": 2.1e-05, "source": "OPENAI", "model": "gpt-6-luna" },
{ "item": "OPENAI_TRANSCRIBE", "qty": 0.1833, "unit": "MINUTE", "rate": 0.0045, "currency": "USD", "amount": 0.00082485, "source": "MEASURED", "model": "gpt-transcribe" } ] }
]
},
"recorded": true,
"script": { "versionId": "e183c7f1-3137-4848-95c1-11a7e8bef17a", "version": 1, "ref": "desk-test" },
"calloutId": null,
"hangupCause": null,
"sipStatus": null,
"meta": null
}
}
| Field | Description |
|---|---|
type / direction |
What kind of call (Part 4); INBOUND = someone called us |
from / to |
E.164 numbers. Incoming: caller → our number. Outgoing: our number → person |
status |
Live state; ENDED once over |
result |
How it went, set when it ends: HANDLED_BY_SCRIPT · HANDLED_BY_AGENT · HANDLED_BY_AI · VOICEMAIL · MISSED · ABANDONED · HUNG_UP_EARLY · REJECTED; outgoing: ANSWERED · NO_ANSWER · BUSY · DECLINED · UNREACHABLE · INVALID_NUMBER · FAILED · NOT_DIALED |
endReason / endedBy |
Why and by whom it ended (CALLER_HUNG_UP / CALLER, SCRIPT_END / SYSTEM…) |
handledBy |
Who dealt with the caller in the end: SCRIPT · AGENT · AI · VOICEMAIL |
answeredAt |
Incoming: when the engine picked up (= start). Outgoing: when the person answered; null if never |
durations (s) |
ringSeconds (outgoing, before answer) · totalSeconds · scriptSeconds (in blocks) · waitSeconds (waiting room) · talkSeconds (with an agent) · holdSeconds · aiSeconds |
keys |
Every key the caller pressed, in order ("1234") |
lastBlock |
The block the call ended in |
outcomes |
Tags the flow set on the way (callback_scheduled, opted_out, an App action's outcome…) |
usage |
Measured units: phone seconds (pstnSeconds, trunkSeconds), ttsChars (new text made into audio for this call), sttSeconds (caller speech transcribed) |
providerCost |
Cost to run it, per provider and line (Part 1 §1.11) |
recorded |
Something was recorded: GET …/recording works |
script |
The flow version it ran |
calloutId |
Set when a call-out placed it |
hangupCause / sipStatus |
Outgoing calls that failed: short network reason (603 Decline, 486 Busy here) and SIP code |
meta |
What the client app attached (call-out meta, OTP meta, scheduled callbacks' {said, scheduledBy}) |
G3. Timeline of a call
Purpose: Everything that happened, step by step: blocks entered, sentences said, keys, speech heard, rings, transfers, recordings. For a call detail screen and for support.
Endpoint: GET {base_url}/calls/{callId}/timeline
Success Response JSON Sample (first events):
{
"success": true,
"httpStatus": "200 OK",
"message": "Timeline",
"action": null,
"action_time": "2026-11-13T20:05:32.529813711",
"data": [
{ "seq": 1, "type": "CALL_RECEIVED", "block": null, "at": "2026-11-13T16:45:27.912895Z", "offsetMs": 3,
"data": { "to": "+255677000001", "from": "+255745051250" } },
{ "seq": 2, "type": "SAY", "block": null, "at": "2026-11-13T16:45:27.915036Z", "offsetMs": 5,
"data": { "text": "Habari, unazungumza na huduma ya kidijitali ya Ringmaster Lab.", "voice": "marin" } },
{ "seq": 4, "type": "BLOCK_ENTER", "block": "hi", "at": "2026-11-13T16:45:27.915108Z", "offsetMs": 5,
"data": { "type": "say" } },
{ "seq": 9, "type": "QUEUED", "block": "desk", "at": "2026-11-13T16:45:40.577Z", "offsetMs": 12668,
"data": { "department": "Mapokezi" } },
{ "seq": 12, "type": "WAIT_OPTION", "block": "desk", "at": "2026-11-13T16:46:03.869Z", "offsetMs": 35960,
"data": { "key": "9", "exit": "vm" } },
{ "seq": 16, "type": "VOICEMAIL", "block": "vm", "at": "2026-11-13T16:46:17.441Z", "offsetMs": 49532,
"data": { "recorded": true } }
]
}
| Field | Description |
|---|---|
seq |
Order (gaps are possible) |
type |
What happened (Part 4 — timeline events) |
block |
The block it happened in, if any |
offsetMs |
Milliseconds since the call started |
data |
Details for that type (texts said, keys, speech, department, exit taken…) |
A SAY event is written when the sentence is queued to play; the audio itself follows in order.
G4. Recording of a call
Purpose: The MP3 of the call's latest recording (a voicemail, or the recorded part with an agent).
Endpoint: GET {base_url}/calls/{callId}/recording
Success Response: 200, Content-Type: audio/mpeg, the audio bytes (not JSON). Stream it to the client app's
users through its own backend (the API key must not reach a browser).
Errors (JSON): 404 "No recording for this call" — nothing recorded, or not ready yet (wait for
recording.ready); 404 "Recording file missing" — deleted after retentionDays.
G5. Transcript and summary
Endpoint: GET {base_url}/calls/{callId}/transcript
Success Response JSON Sample:
{
"success": true,
"httpStatus": "200 OK",
"message": "Transcript",
"action": null,
"action_time": "2026-11-13T20:05:32.551771069",
"data": {
"callId": "355760f8-51c0-47c8-b3b0-49c3612e4b96",
"status": "READY",
"language": "sw",
"text": "Huu ndio ujumbe wangu, na shukrani sana.\n",
"summary": "Mpigaji amesema huo ndio ujumbe wake na ametoa shukrani nyingi.",
"sentiment": "positive",
"unanswered": [],
"fields": {},
"model": "gpt-6-luna",
"updatedAt": "2026-11-13T16:46:27.176884Z"
}
}
| Field | Description |
|---|---|
status |
READY · FAILED (transcription failed; text is empty) |
language |
Detected language (sw, en) |
text |
What was said |
summary |
One or two sentences, in the call's language |
sentiment |
positive · neutral · negative |
unanswered |
Questions the caller asked that the AI could not answer (for the knowledge base) |
fields |
Values the flow asked the summary to pick out (e.g. {"amount": "45000"}), with fixed English keys |
Ready when the summary.ready webhook arrives. Before that, and when nothing was recorded or recording.transcribe
is off: 404 "No transcript for this call".
G6. Export calls (CSV)
Endpoint: GET {base_url}/calls/export.csv
| Query | Type | Required | Description | Default |
|---|---|---|---|---|
accountId |
uuid | Yes | The account | |
from / to |
date | No | Days YYYY-MM-DD, account time; at most 400 days |
the last 7 days |
Success Response: 200, text/csv attachment, up to 100,000 rows, oldest first. Header row:
callId,type,direction,fromE164,toE164,status,result,endReason,endedBy,handledBy,startedAt,answeredAt,endedAt,totalSeconds,waitSeconds,talkSeconds,aiSeconds,pstnSeconds,trunkSeconds,ttsChars,sttSeconds,hangupCause,sipStatus,keys,lastBlock,calloutId,agentRef,outcomes,meta
G7. Agent calls a number (click to call)
Purpose: An agent calls a customer from the browser, from one of the account's numbers. The engine dials; the agent joins the call room with the token returned and hears it ring.
Endpoint: POST {base_url}/calls/agent
Request JSON Sample:
{
"accountId": "adb72fac-66a2-471b-a437-035fe008197d",
"numberId": "8a8d9c13-bda2-4a18-8410-c5ec49ed2c66",
"to": "+255745051250",
"agentRef": "asha",
"meta": { "ticket": "T-1042" }
}
| Parameter | Type | Required | Description |
|---|---|---|---|
accountId |
uuid | Yes | The account |
numberId |
uuid | Yes | Calling from (one of the account's numbers) |
to |
string | Yes | Tanzanian number |
agentRef |
string | Yes | The agent who calls |
meta |
object | No | Kept on the call, returned in its summary and webhooks |
Success Response JSON Sample (201 Created):
{
"success": true,
"httpStatus": "201 CREATED",
"message": "Calling",
"action": null,
"action_time": "2026-11-13T20:10:00.000000000",
"data": {
"callId": "6a1f…",
"room": { "url": "wss://<livekit-host>", "room": "call-6a1f…", "identity": "agent-0ea57aeb-…", "token": "eyJhbGciOi…" }
}
}
The browser joins room with token (LiveKit JS SDK, microphone on). The agent is BUSY until the call ends.
Errors: 404 "Agent not found: asha" · 409 AGENT_BUSY · 403 CAN_CALL_REFUSED (the client app's can-call
check said no, Part 3) · 400 "Only Tanzanian numbers can be called".
H. Call-outs (calling people)
Rules applied to every call-out (quiet hours, opt-outs, retries, pace): Part 1 §1.5.
Call-outs play an Outgoing flow; its vars must be given for every person.
Quick call: POST /v1/calls ─────────────────────────────── (created + people added + started in one go)
Step by step: POST /v1/callouts → POST …/{id}/recipients (one or more times) → POST …/{id}/start
DRAFT ─start─► SCHEDULED (startsAt later) ─► RUNNING ⇄ PAUSED ─► COMPLETED
│ │ │
└──cancel──► CANCELLED ◄──cancel──┘ └──stop──► STOPPED
H1. Quick call
Purpose: Call 1–1000 people now with one request ("Habari Asha, oda yako iko tayari").
Endpoint: POST {base_url}/calls
Request JSON Sample:
{
"accountId": "51f815ba-e9eb-4e29-9297-3c56033d32cd",
"scriptVersionId": "9f0d2a77-…",
"fromNumberId": "2c4e…",
"name": "Order ready",
"to": [
{ "e164": "+255745051250", "vars": { "product": "Acetic Acid", "qty": "20" }, "meta": { "orderId": "ORD-88" } },
{ "e164": "+255712345678", "vars": { "product": "Zinc Dust", "qty": "5" } }
],
"maxTries": 3,
"retryGapSec": 300,
"meta": { "batch": "evening" }
}
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
accountId |
uuid | Yes | The account | |
scriptVersionId |
uuid | Yes | A version of an Outgoing flow of the account | not revoked |
fromNumberId |
uuid | Yes | Caller ID: one of the account's numbers | |
to |
array | Yes | People: {e164, vars?, meta?} |
1–1000 |
to[].vars |
object | When the flow has vars |
Values the flow says ({product}) |
every flow var, not blank |
to[].meta |
object | No | Kept on the person and their calls | |
name |
string | No | Shown in lists | default "API broadcast" |
maxTries |
integer | No | Tries per person (busy / no answer / unreachable / failed) | 1–3, default 1 |
retryGapSec |
integer | No | Wait between tries | 300–86400, default 300 |
meta |
object | No | Kept on the call-out |
Success Response: 201 Created, message "Calling", data = the call-out (H7), already
RUNNING (or SCHEDULED in quiet hours: it waits).
Errors:
403 FEATURE_OFF"Call-outs are not enabled for this client app" (noCALLOUTSfeature)400"Call-outs run Outgoing scripts only" · "Script version belongs to another account"409 VERSION_REVOKED422 VALIDATION_FAILED"Some people have no value for a script variable",data: [{"e164": "+2557…", "missing": "product"}]422 VALIDATION_FAILED"Nobody can be called",data= the skipped list (H3)
H2. Create a call-out
Purpose: A broadcast built in steps, or a campaign (a list called over days, in a window, at a pace).
Endpoint: POST {base_url}/callouts
Request JSON Sample (campaign):
{
"accountId": "51f815ba-e9eb-4e29-9297-3c56033d32cd",
"externalRef": "debt-reminder-2026-11",
"name": "Debt reminder November",
"kind": "CAMPAIGN",
"scriptVersionId": "9f0d2a77-…",
"fromNumberId": "2c4e…",
"startsAt": "2026-11-14T06:00:00Z",
"endsAt": "2026-11-21T15:00:00Z",
"windowFrom": "09:00",
"windowTo": "17:00",
"days": [1, 2, 3, 4, 5],
"maxTries": 3,
"retryGapSec": 3600,
"pacePerMin": 30,
"fallback": { "action": "send_sms", "params": { "text": "Habari {name}, tafadhali lipa deni lako la {amount}." } },
"meta": { "segment": "overdue-30" }
}
| Parameter | Type | Required | Description | Validation / default |
|---|---|---|---|---|
accountId |
uuid | Yes | ||
name |
string | Yes | not blank | |
kind |
string | Yes | BROADCAST · CAMPAIGN |
|
scriptVersionId |
uuid | Yes | Outgoing flow version. May still wait for approval: the call-out shows waitingApproval: true and starts once approved |
not revoked |
fromNumberId |
uuid | Yes | Caller ID | the account's number |
externalRef |
string | No | The client app's id | |
startsAt |
instant | No | Don't call before (start makes it SCHEDULED) |
default: at start |
endsAt |
instant | No | Campaign: stop for good after this (STOPPED) |
|
windowFrom / windowTo |
HH:mm |
No | Campaign: only call between these each day (account time) | both or neither |
days |
int[] | No | Campaign: week days, 1 = Monday … 7 = Sunday | |
quietOverride |
boolean | No | Call even in quiet hours (urgent notices only) | default false |
maxTries |
integer | No | Tries per person | 1–5, default 1 |
retryGapSec |
integer | No | Wait between tries | ≥ 300, default 3600 |
pacePerMin |
integer | No | New calls per minute | 1–600, default 60 |
fallback |
object | No | Campaign: after a person's last failed try, ask the client app to run one of its registered actions ({action, params}); not one that waits |
|
meta |
object | No |
Success Response: 201 Created, message "Call-out created", data = the call-out, status: "DRAFT".
Errors: 400 "Give both windowFrom and windowTo" · "Window times must be HH:mm" · "Days are 1 (Monday) to 7 (Sunday)" ·
"fallback.action must be one of the client app's registered actions" · "A fallback action can't be one that waits for an outcome" ·
403 FEATURE_OFF · the same flow errors as H1.
H3. Add people
Purpose: Add people to a DRAFT call-out (call it several times for long lists).
Endpoint: POST {base_url}/callouts/{id}/recipients
Request JSON Sample:
{
"recipients": [
{ "e164": "+255745051250", "vars": { "name": "Asha", "amount": "45000" }, "meta": { "customerId": "C-77" } },
{ "e164": "0712345678", "vars": { "name": "Juma", "amount": "12000" } },
{ "e164": "+14155550100", "vars": { "name": "Tom", "amount": "1" } }
]
}
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
recipients |
array | Yes | {e164, vars?, meta?} |
≤ 10,000 per request; every flow var given |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "200 OK",
"message": "Recipients added",
"action": null,
"action_time": "2026-11-13T20:12:00.000000000",
"data": {
"added": 2,
"skipped": [ { "e164": "+14155550100", "reason": "NOT_TANZANIAN" } ]
}
}
skipped[].reason |
Means |
|---|---|
INVALID_NUMBER |
Not a phone number |
NOT_TANZANIAN |
Only +255 numbers are called |
DUPLICATE |
Already on this call-out (or twice in the request) |
OPTED_OUT |
On the account's opt-out list: kept as SKIPPED with result OPTED_OUT, never called |
Local numbers (0712…) are turned into E.164.
Errors: 409 LIST_LOCKED "The list is locked once a call-out starts; create a new call-out" ·
422 VALIDATION_FAILED "Some people have no value for a script variable" (nobody added).
H4. Start, pause, resume, stop, cancel
Endpoint: POST {base_url}/callouts/{id}/{action}
{action} |
From → to | Refused with |
|---|---|---|
start |
DRAFT → RUNNING (or SCHEDULED when startsAt is later). Locks the list |
409 ALREADY_STARTED · 409 NO_RECIPIENTS "Nobody to call" |
pause |
RUNNING / SCHEDULED → PAUSED; calls in progress finish. Optional body {"reason": "…"} (default PAUSED_BY_CLIENT) |
409 NOT_RUNNING |
resume |
PAUSED → RUNNING |
409 NOT_PAUSED |
stop |
any unfinished → STOPPED; people not yet called are not called. Stopping a finished call-out changes nothing |
— |
cancel |
DRAFT / SCHEDULED → CANCELLED (nothing was dialled) |
409 ALREADY_RUNNING "A running call-out is stopped, not cancelled" |
Success Response: 200, message "Call-out start" (… "pause", "resume", "stop", "cancel"), data = the call-out.
Unknown action → 400 "Unknown action: explode".
The engine itself also pauses call-outs, with a pauseReason: ACCOUNT_SUSPENDED, VERSION_REVOKED, FEATURE_OFF
(the owner took CALLOUTS away), … and sends
callout.paused (Part 3). It never resumes them by itself.
H5. List call-outs
Endpoint: GET {base_url}/callouts?accountId={accountId}
| Query | Type | Required | Description |
|---|---|---|---|
accountId |
uuid | Yes | Missing → 400 Missing parameter: accountId |
Success Response: data = array of call-outs, newest first. Scheduled callbacks booked by callers are here too,
with externalRef schedule:<flow ref>:<number>.
H6. Get a call-out's people
Endpoint: GET {base_url}/callouts/{id}/recipients
| Query | Type | Required | Description | Default |
|---|---|---|---|---|
status |
string | No | PENDING · DIALING · DONE · SKIPPED |
all |
page / size |
integer | No | Page from 0; size up to 500 | 0 / 100 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "200 OK",
"message": "Recipients",
"action": null,
"action_time": "2026-11-13T20:05:37.426862727",
"data": [
{
"id": 11,
"e164": "+255745051250",
"status": "DONE",
"result": "ANSWERED",
"attempts": 1,
"lastCallId": "fff4aa3f-8ed5-44e6-947c-c82bb3628c41",
"answeredSeconds": 28,
"keys": "1234",
"outcomes": null,
"meta": { "said": "After five minutes, baada ya dakika tano.", "scheduledBy": "ca10b2f7-a5a3-41bb-be8b-2e486b2b5dbf" },
"nextAttemptAt": null
}
]
}
| Field | Description |
|---|---|
status |
PENDING (waiting or between tries) · DIALING · DONE · SKIPPED |
result |
The last try's result (ANSWERED, NO_ANSWER, BUSY, DECLINED, OPTED_OUT…) |
attempts / nextAttemptAt |
Tries so far; when the next try is due (while PENDING) |
lastCallId |
The last call to this person (G2) |
answeredSeconds / keys / outcomes |
From the answered call |
H7. Get a call-out
Endpoint: GET {base_url}/callouts/{id}
Success Response JSON Sample (a callback a caller booked for "baada ya dakika tano"):
{
"success": true,
"httpStatus": "200 OK",
"message": "Call-out",
"action": null,
"action_time": "2026-11-13T20:05:37.397238463",
"data": {
"id": "510fca7b-b226-46ad-b80a-0082ab4ab0d5",
"externalRef": "schedule:account-callback:+255745051250",
"name": "Callback +255745051250",
"kind": "BROADCAST",
"status": "COMPLETED",
"pauseReason": null,
"waitingApproval": false,
"scriptVersionId": "3bcb496b-e302-4a98-9e8e-fd3e4c80b768",
"startsAt": "2026-11-13T14:41:00Z",
"endsAt": null,
"windowFrom": null,
"windowTo": null,
"days": null,
"maxTries": 2,
"retryGapSec": 300,
"pacePerMin": 60,
"total": 1,
"byStatus": { "DONE": 1 },
"byResult": { "ANSWERED": 1 },
"byOutcome": {},
"createdAt": "2026-11-13T14:36:38.242481Z",
"startedAt": "2026-11-13T14:36:38.282846Z",
"completedAt": "2026-11-13T14:41:34.766757Z"
}
}
| Field | Description |
|---|---|
status / pauseReason |
See the diagram above |
waitingApproval |
The flow version is not runnable yet; nobody is called until it is approved |
days |
Campaign days as text ("1,2,3,4,5") |
total / byStatus / byResult / byOutcome |
Counts over its people |
H8. Call-out stats
Endpoint: GET {base_url}/callouts/{id}/stats
Success Response JSON Sample:
{
"success": true,
"httpStatus": "200 OK",
"message": "Call-out stats",
"action": null,
"action_time": "2026-11-13T20:05:37.463714267",
"data": {
"calloutId": "510fca7b-b226-46ad-b80a-0082ab4ab0d5",
"total": 1,
"byStatus": { "DONE": 1 },
"byResult": { "ANSWERED": 1 },
"byOutcome": {},
"byAttempts": { "1": 1 },
"byHour": [
{ "key": "2026-11-13T17:00", "values": { "dialed": 1, "answered": 1, "noAnswer": 0, "busy": 0, "failed": 0, "seconds": 28 } }
]
}
}
| Field | Description |
|---|---|
byAttempts |
People by the try that finished them ({"1": 80, "2": 15, "3": 5}) |
byHour[].key |
Hour in the account's time zone |
byHour[].values |
Calls dialled, answered, not answered, busy, failed; answered seconds |
H9. Export a call-out (CSV)
Endpoint: GET {base_url}/callouts/{id}/export.csv
Success Response: text/csv attachment, one row per person:
e164,status,result,attempts,answeredSeconds,keys,outcomes,callId,meta,updatedAt
+255745051250,DONE,ANSWERED,1,28,1234,,fff4aa3f-8ed5-44e6-947c-c82bb3628c41,"{""said"": ""After five minutes, baada ya dakika tano."", ""scheduledBy"": ""ca10b2f7-…""}",2026-11-13T14:41:34.763432Z
M. OTP (a code by voice)
M1. Call a code to a number
Purpose: Read a one-time code to a person ("Namba yako ya siri ni moja, mbili, tatu, nne"), e.g. when SMS fails. Not subject to quiet hours. The code is kept encrypted and never shown in timelines or logs.
Endpoint: POST {base_url}/otp
Access Level: 🔒 Protected · Bearer API key · feature OTP
Request JSON Sample:
{
"accountId": "adb72fac-66a2-471b-a437-035fe008197d",
"to": "+255745051250",
"code": "482913",
"lang": "sw",
"numberId": "8a8d9c13-bda2-4a18-8410-c5ec49ed2c66",
"meta": { "purpose": "login" }
}
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
accountId |
uuid | Yes | ||
to |
string | Yes | Tanzanian number | |
code |
string | Yes | Digits only | 4–10 digits |
lang |
string | No | sw · en |
default: the account's language |
numberId |
uuid | No | Calling from | default: the account's first number |
meta |
object | No | Kept on the call |
Success Response JSON Sample (201 Created):
{
"success": true,
"httpStatus": "201 CREATED",
"message": "Calling",
"action": null,
"action_time": "2026-11-13T20:15:00.000000000",
"data": { "callId": "a7c3…", "status": "CALLING" }
}
Follow the call with call.answered / call.ended webhooks (type OTP).
Errors:
{
"success": false,
"httpStatus": "422 UNPROCESSABLE_CONTENT",
"message": "Validation failed",
"code": "VALIDATION_FAILED",
"action_time": "2026-11-13T20:05:41.000000000",
"data": { "code": "must match \"\\d{4,10}\"" }
}
400 "Only Tanzanian numbers can be called" · 400 "This account has no number to call from" ·
403 FEATURE_OFF · 429 RATE_LIMITED "Too many codes to this number; try again later" (otpPerDestinationPer10Min).
N. Account stats
N1. Calls per day, hour, number or type
Purpose: A business dashboard: how many calls, how they ended, how long.
Endpoint: GET {base_url}/accounts/{accountId}/stats
| Query | Type | Required | Description | Default |
|---|---|---|---|---|
from / to |
date | No | YYYY-MM-DD, account time; at most 400 days |
the last 7 days |
groupBy |
string | No | day · hour · number · type |
day |
Success Response JSON Sample (groupBy=day):
{
"success": true,
"httpStatus": "200 OK",
"message": "Account stats",
"action": null,
"action_time": "2026-11-13T20:05:41.000000000",
"data": [
{ "key": "2026-11-13", "values": { "incoming": 38, "outgoing": 2, "answered": 39, "byScript": 11, "byAgent": 3,
"byAi": 9, "voicemail": 6, "missed": 0, "abandoned": 1, "hungUpEarly": 8, "seconds": 2311.0 } }
]
}
values (day / hour / number) |
Description |
|---|---|
incoming / outgoing |
Calls in / out |
answered |
Answered calls |
byScript / byAgent / byAi / voicemail |
Who dealt with them |
missed |
Callers who waited for an agent and nobody answered |
abandoned |
Callers who hung up while waiting |
hungUpEarly |
Callers who hung up in the first seconds |
seconds |
Total call seconds |
With groupBy=type, key is the call type and values = {calls, answered, seconds, pstnSeconds}:
[ { "key": "BROADCAST", "values": { "calls": 2, "answered": 1, "seconds": 28, "pstnSeconds": 28 } },
{ "key": "INCOMING", "values": { "calls": 38, "answered": 38, "seconds": 2283, "pstnSeconds": 2283 } } ]
Errors: 400 "groupBy must be day, hour, number or type".
Agents' stats: Part 2D.