Skip to main content

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" (no CALLOUTS feature)
  • 400 "Call-outs run Outgoing scripts only" · "Script version belongs to another account"
  • 409 VERSION_REVOKED
  • 422 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.