Skip to main content

Part 2A — Setup: basics, accounts, numbers

← Contents · Previous: Part 1 · Next: Part 2B2A — Call flowsSetup

EveryRead endpointthis herepage needsbefore Authorization:the Bearerendpoints. <apiRingmaster key>is (🔒).the Theengine envelopeof an intelligent call centre; your service is a client app built on it. This page explains the engine's objects, how a call runs, and error codes are on the contents page.


A. Basics

A1. Ping

Purpose: Check the key works and see which featureswhat the client app has.must do at each step.

Endpoint:


GET

1.1 {base_url}/ping

Who's

Access Level: 🔒 Protected · Bearer API key

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "200 OK",
  "message": "pong",
  "action": null,
  "action_time": "2026-11-13T19:57:58.900619432",
  "data": {
    "clientAppId": "bd74035b-bdb6-450d-885b-222e83719906",
    "name": "softphone-test",
    "features": ["INBOUND", "CALLOUTS", "OTP", "AI"]
  }
}
who

FieldWho DescriptionWhat they areExample
Ringmaster ownerRuns the engine; creates client apps, buys phone numbers, gives numbers to client apps (ringmaster-admin, see Appendix)QBIT SPARK ops
Client appA product that uses the engine through this API. Has one API key, a webhook URL and a set of featuresa messaging or business platform
AccountOne business inside a client app. Everything (numbers, flows, calls, agents) belongs to an accountPMK Chemicals, Kliniki ya Afya Bora
NumberA phone number the account answers on and calls from+255677000010
Call flow (script)What a caller hears and can do, as wired blocks. Stored in numbered, unchangeable versionsorders v3
AgentA person who answers calls in the browser (or on a mobile). Ringmaster keeps only a ref and a nameasha
DepartmentA group of agents a flow can send callers toMapokezi, Uhasibu
Caller / person calledAnyone on a phone. Ringmaster keeps only the number and what happened on the call+255745051250
Ringmaster owner
 └── Client app  (API key, webhook URL, features)
      ├── Account: PMK Chemicals  (hours, quiet hours, recording, limits)
      │    ├── Numbers ── each plays one pinned flow version
      │    ├── Call flows ── versions v1, v2, v3 …
      │    ├── Departments ── agents
      │    ├── Media (audio files), knowledge base (for the AI), opt-outs
      │    └── Calls, call-outs, reports
      └── Account: Kliniki ya Afya Bora …

A client app maps its own objects to these: one of its businesses = an account (externalRef = its business id), one of its users who answers calls = an agent (ref = its user id).


1.2 Getting access

The Ringmaster owner creates the client app once and gives it:

ItemWhat it isWhere it's used
API keyvk_live_…, shown onceEvery request: Authorization: Bearer <key>
Webhook URLThe client app's HTTPS endpoint, e.g. https://app.example.co.tz/hooks/ringmasterThe engine POSTs events there (Part 3)
Webhook secretShown once; signs every webhookThe client app checks each event's signature
FeaturesWhat the app may doSee below
Rate limitRequests per second for the app (fixed one-second window)Over it → 429 RATE_LIMITED
CALLOUTS,OTP, (Part
FeatureAllows
clientAppIdINBOUND TheAnswering clientincoming appcalls. thisWithout keyit belongsevery toincoming call is refused (REJECTED, FEATURE_OFF)
nameCALLOUTS ItsCalling namepeople: atquick thecalls Ringmasterand ownercall-outs (403 FEATURE_OFF without it); callers booking a callback (refused without it)
featuresOTP WhatVoice OTP calls (POST /v1/otp); without it may do:→ INBOUND403 FEATURE_OFF,
AI, AI Agent blocks and "smart" (AI) speech matching in Menu, Poll and Pick; a flow using them is refused (403 FEATURE_OFF)
NUMBERS Adding 1and §1.2)removing numbers through the API; without it → 403 FEATURE_OFF

Features are checked when something is made Errorand Responseagain JSONwhen Sampleit runs (no or wrong key):

{
  "success": false,if "httpStatus": "401 UNAUTHORIZED",
  "message": "Missing or invalid API key",
  "code": "UNAUTHORIZED",
  "action_time": "2026-11-13T19:57:59.067583389",
  "data": null
}

Also: 403 CLIENT_SUSPENDED (the owner suspendedtakes a feature away later, running call-outs pause (FEATURE_OFF), AI Agent blocks take their err exit, and smart speech matching falls back to the app),options' own words. Agent click-to-call (POST /v1/calls/agent) needs no feature.

A client app only ever sees its own accounts, numbers and calls. Another app's id gives 429404 RATE_LIMITEDNOT_FOUND.


A2.1.3 ScriptSetting schemaup a business, in order

1. POST  /v1/accounts                         {externalRef: "pmk-chemicals"}        → accountId
2. PATCH /v1/accounts/{accountId}             {businessName, hours, quietHours, …}
3. GET   /v1/numbers?status=ALLOCATED          free numbers the owner gave the client app
   POST  /v1/numbers/{numberId}/assign         {accountId, label}
4. PUT   /v1/accounts/{accountId}/scripts/orders            {name, direction: "INCOMING"}
   POST  /v1/accounts/{accountId}/scripts/orders/versions   {version: 1, definition, runnable: true}  → versionId
5. PUT   /v1/numbers/{numberId}/script         {scriptVersionId}

Purpose:From Thestep 5 on, calls to the number play the flow. Optional: departments and agents (to connect callers to people), media (recorded greetings, music), knowledge base (for AI agents), App actions (SMS, payments done by the client app).


1.4 An incoming call, step by step

Caller dials +255677000010
   │  phone network → SIP trunk → LiveKit room → voice worker joins
   ▼
Engine: which account and which flow version is pinned on the number?   (none → call refused, NO_SCRIPT)
   │  checks: app active, account active, INBOUND on, caller under the per-caller limit
   ▼
call.started webhook ──────────────────────────────────────────► client app
   │
   ▼  blocks run one after another, starting at the flow's start block
   cataloguesay a"Karibu flowPMK editorChemicals…" needs:→ menu "Bonyeza 1 kuagiza…" → (caller presses 1 or says "oda")
   │      every blockstep type,is itswritten group, what it does, its exits, whether
it needs approval;to the rules;call's timeline (BLOCK_ENTER, SAY, KEYS, SPEECH, …)
   │      call.block webhooks for the messagesteps shape;clients thecare variablesabout a(menu flowchoice, canApp use;action, an…)
   example▼
   flow.

… collect → pick → confirm → goodbye ▼ Call ends ─► call.ended webhook (full summary: result, durations, keys, outcomes, cost) └─► recording.ready / summary.ready later, if anything was recorded

Endpoint: GET {base_url}/scripts/schema

Access Level: 🔒 Protected · Bearer API key

Success Response JSON Sample (shortened: 18 blocks in blocks):

{
  "success": true,
  "httpStatus": "200 OK",
  "message": "Script schema",
  "action": null,
  "action_time": "2026-11-13T19:58:20.112842101",
  "data": {
    "schemaVersion": "1",
    "blocks": [
      { "type": "say", "group": "Speak", "description": "Say a text or play an audio message, then continue.",
        "exits": ["next"], "needsApproval": false },
      { "type": "menu", "group": "Ask",
        "description": "AskWhat the caller tohears pickis played in order, block after block: a block's sound finishes before the next block's
sound starts. Blocks that make something happen for a person wait for that too: a Connect block rings agents only
after the caller has heard everything before the waiting music.

Statuses of a call (status): QUEUED → DIALING → RINGING (outgoing only) → IN_SCRIPT → WAITING (waiting room) → CONNECTING → LIVE (with an optionagent) by→ keyENDED. or,After whenit speechends, isresult on,says byhow voiceit …"went (HANDLED_BY_SCRIPT, "exits": ["<option>"HANDLED_BY_AGENT, "noInput"HANDLED_BY_AI, "notUnderstood"VOICEMAIL, "later"]MISSED, "needsApproval": false }NO_ANSWER, { "type": "collect"BUSY, "group": "Ask"DECLINED…), "description":endReason "Collectwhy ait numberended and endedBy who ended it. All values: Part 4.

Guards that protect every account (account,account PIN, amount) into {input.<name>}."limits, "exits"changeable with PATCH /v1/accounts/{id}): ["ok", "failed", "later"], "needsApproval": true } ], "rules": [ "direction is INCOMING (start exit 'call') or OUTGOING (start exit 'out'); never both", "every block: {type, …settings, before?: message, wr?: waiting room, x: {exit: blockId}}; a missing exit ends the call", "at most 60 blocks, 10 menu keys, 3 retries; text at most 500 characters" ], "messageShape": { "text": { "src": "text", "text": "Habari {name}", "lang": "sw", "voice": "marin|coral|shimmer|sage|cedar|ash|echo|ballad|verse", "speed": "0.7-1.5 (1.0 normal)…" }, "audio": { "src": "record|upload|library", "media": "<mediaId>" } }, "systemVariables": ["date", "time", "lang", "answeredBy", "key", "heard", "number", "caller", "callId"], "example": { "direction": "INCOMING", "start": { "call": "greet" }, "blocks": { "greet": { "type": "say", "msg": { "src": "text", "text": "Karibu!" }, "x": { "next": "menu" } }, "menu": { "type": "menu", "msg": { "src": "text", "text": "Bonyeza moja kwa mauzo, mbili kwa msaada." }, "options": [ { "k": "1", "id": "sales", "label": "Mauzo", "words": ["mauzo", "kununua"] }, { "k": "2", "id": "help", "label": "Msaada", "words": ["msaada", "tatizo"] } ], "x": { "sales": "sales", "help": "help", "noInput": "bye", "notUnderstood": "bye" } }, "sales": { "type": "say", "msg": { "src": "text", "text": "Bei zetu ziko kwenye tovuti yetu." }, "x": { "next": "bye" } }, "help": { "type": "say", "msg": { "src": "text", "text": "Mhudumu atakupigia leo." }, "x": { "next": "bye" } }, "bye": { "type": "goodbye", "msg": { "src": "text", "text": "Asante, kwaheri." } } } } } }

available
FieldLimit DescriptionDefaultWhat happens
blocks[].exitsmaxCallMinutes Exit names. <option> = one exit per menu/connect option (its id); <outcome> = one per App action outcome; <return> = the names a transferred flow returns with
blocks[].needsApproval15 A risky block: a version using it should be approved before it runs. The enginecall reportsis these in riskyBlocks; the client app pushes such versions with runnable: false and approves them laterended (Part 2B, D7MAX_CALL_LENGTH)
systemVariablesmaxWaitSeconds Always300 Longest wait in textsa aswaiting room, whatever the flow says
{name}callerCallsPer10Min: 5The same caller calling more often is refused ({caller}REJECTED, {date}CALLER_LIMIT,)
{time}otpPerDestinationPer10Min, 3More OTP calls to one number → refused
{lang}voicemailMaxSeconds, 120Longest voicemail
{key}maxBlockVisits25Blocks run without any input from the caller before the call is ended (last key), {heard} (last speech), {number} (number called), {callId}, {answeredBy} (outgoing, with answering-machine detection: person / machineLOOP_GUARD) — stops endless loops

1.5 Calling people (call-outs)

The fullengine meaningplaces ofcalls everyfor blockan settingaccount isand inplays SCRIPTS.md.an TheOutgoing schemaflow is generated fromwhen the engine'sperson code, so it always matches what the engine accepts; cache it per schemaVersion.answers.


A3. Check a script (dry run)

Purpose: Validate a flow document without storing it: the same checks as pushing a version. Use it in the editor on every save.

Endpoint: POST {base_url}/scripts/validate

Access Level: 🔒 Protected · Bearer API key

Request JSON Sample:

{
  "accountId": "adb72fac-66a2-471b-a437-035fe008197d",
  "definition": {
    "direction": "INCOMING",
    "start": { "call": "a" },
    "blocks": {
      "a": { "type": "say", "msg": { "src": "text", "text": "Hi" }, "x": { "next": "zz" } }
    }
  }
}
"Yourorder names
ParameterWay TypeEndpoint RequiredDescriptionUse
Quick calldefinitionPOST /v1/calls object Yes Theis flowready" documentto (SCRIPTS.md)1–1000 people, now. Created and started in one request
BroadcastaccountIdPOST /v1/callouts kind: BROADCAST uuidThe same, but built in steps (create → add people → start), optionally at startsAt
Campaign NoPOST /v1/callouts kind: CAMPAIGN WithCalling it,a referenceslist areover checkeddays: too:a departments,daily media,window (windowFrom/windowTo), week days, pace per minute, up to 5 tries, a fallback App action (e.g. SMS) after the flowslast failed try
Scheduled callbackmade by a Transfercaller during a call (Schedule call block or ScheduleAI blockschedule_call) "Nipigie baada ya saa moja": a one-person call-out with ref schedule:<flow>:<number>; the client app sees it in GET /v1/callouts and the call.scheduled webhook
OTPPOST /v1/otpRead a code to a number

Each person on a call-out is a Successrecipient Responsewith JSONtheir Sampleown vars (e.g. {"name": "Asha", "amount": "45000"}) that the flow says as {name}, {amount}.

Rules the engine applies to every call-out (the checkclient ran;app thisdoesn't need to):

  • Quiet hours (account quietHours, default 21:00–07:00): nobody is called inside them; calls wait for the end.
  • Opt-outs: a number on the account's opt-out list is skipped (SKIPPED), checked again just before dialling.
  • Retries: busy, no answer, unreachable or failed → tried again after retryGapSec (≥ 300 s) up to maxTries (quick call ≤ 3, campaign ≤ 5). Declined (the person rejected the call) is never retried.
  • Pace: campaigns dial at most pacePerMin.

Call-out statuses: DRAFT → SCHEDULED → RUNNING ⇄ PAUSED → COMPLETED / STOPPED / CANCELLED. Recipient: PENDING → DIALING → DONE (with a result) or SKIPPED.


1.6 A call flow's life

PUT  …/scripts/{ref}                    create the flow (name, INCOMING or OUTGOING); direction never changes
POST …/scripts/{ref}/versions           push version 1, 2, 3 … — each version is frozen once stored
         │  the engine checks it (Part 5 §5.9): errors → 422 with the list, nothing stored
         │                                       warnings → stored, warnings returned
         ▼
      runnable?  ── no (you pushed runnable:false: it has risky blocks and waits for approval)
         │              POST …/versions/{n}/runnable  after the client app's approval step
         ▼ yes
PUT  /v1/numbers/{id}/script            put the version on a number (Incoming)     — or —
POST /v1/calls · /v1/callouts           use it for calls out (Outgoing)
POST …/versions/{n}/revoke              stop a version for good: a number still playing it refuses calls,
                                        call-outs using it pause (VERSION_REVOKED) — move them first
DELETE …/scripts/{ref}                  delete the flow (refused while anything uses it; archived if it ever ran)
  • Versions never change. To change a flow, push the next version and move the number to it. Calls keep pointing at the exact version they ran, so reports stay true.
  • Pushing the same version number with the same document again is safe: 200 "Version already stored".
  • Approval is the client app's policy. Some blocks are risky: collect (callers' digits), request (calls a URL), ai_agent, and App actions the client app registered as risky (e.g. pay). The engine lists them in riskyBlocks when a version is checked or stored, and trusts the runnable flag it is given: push such a version with runnable: false, let the business's reviewer approve it in the client app, then POST …/versions/{n}/runnable. A not-runnable version can't be put on a number. A call-out may already use it: it is saved and waits (waitingApproval: true), then starts by itself once the version is approved.
  • testOnly versions are for test calls only and never become runnable.
  • Reading a flow back: GET …/scripts/{ref}/versions/{n} returns the full document. Secrets stored in Send request blocks come back as "(hidden)".
  • Layout (block positions on the client app's canvas) belongs to the client app: the engine drops a layout key on push.
  • Texts are turned into speech when the version is stored, so the first caller doesn't wait.

1.7 Agents in the browser

Client app (once): POST departments {name, strategy, ringTimeoutSec…}  ·  PUT agents/{ref} {displayName}
                 PUT departments/{id}/members {agentRefs: [...]}      (list order = round-robin order)

Agent's browser:
  "Start shift"  → client app backend → POST agents/{ref}/lobby-token → {url, room, token}
                 → browser joins the lobby room (LiveKit JS SDK, data only, no mic)
                 → client app backend → PUT agents/{ref}/status {AVAILABLE}
  A caller reaches a Connect block → hears "tunakuunganisha…", then music
                 → engine sends {type:"ring", callId, from, department} into the lobby
  "Accept"       → client app backend → POST /v1/calls/{callId}/accept {agentRef} → {url, room, token}
                 → browser joins the call room with the microphone on → they talk
  "Hold" / "Transfer" / "Hang up" → POST /v1/calls/{callId}/hold · /transfer · /hangup {agentRef}
  Call ends      → agent is WRAP_UP for the department's wrapUpSec, then AVAILABLE (or POST …/ready)

Agent statuses: OFFLINE → AVAILABLE → RINGING → BUSY → WRAP_UP → AVAILABLE; AWAY = on a break (online, no calls). One call at a time per agent. Missing maxMissedBeforeAway rings in a row (default 5) moves the agent to AWAY. Every change is sent as an error)agent.status webhook (for a wallboard).

Ring modes (per number, PUT /v1/numbers/{id}/ring-mode): BROWSER (agents' browsers), MOBILE (agents' mobiles), BROWSER_AND_MOBILE.

The browser needs HTTPS (or localhost) for the microphone, and the "Start shift" click is what allows the ring sound. Details of every signal: Part 3.


1.8 Webhooks

Every event is a signed POST to the client app's webhook URL:

POST https://app.example.co.tz/hooks/ringmaster
Content-Type: application/json
X-Ringmaster-Event: call.ended
X-Ringmaster-Delivery: 5b0c7f8e-…            (same as the body's id; use it to drop repeats)
X-Ringmaster-Timestamp: 1794588244
X-Ringmaster-Signature: 9f2c…                 hex HMAC-SHA256(secret, "<timestamp>.<raw body>")

{ "success": true,
  "httpStatus"id": "200 OK"5b0c7f8e-…", "message"type": "Validation result"call.ended", "action": null,
  "action_time"createdAt": "2026-11-13T18:06:38.004985164"13T16:44:04Z", "data": { "valid": false,
    "errors": [
      { "level": "ERROR", "block": "a", "code": "EXIT_TARGET_MISSING", "message": "Exit 'next' points to a missing block: zz" }
    ],
    "warnings": [],
    "vars": [],
    "texts": ["Hi"],
    "riskyBlocks": []… } }
  • Answer 2xx quickly (do the work after). Anything else is retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, then every 12 h until 24 h have passed; then the event is marked failed.
  • Events are written in the same database step as the change they describe, so none is lost; a repeat can arrive — drop it by id.
  • Check the signature on the raw body before parsing, and refuse timestamps more than 5 minutes old.

All events and their data: Part 3.


1.9 App actions (the client app does something during a call)

Ringmaster never sends SMS, takes payments or tags contacts itself. The client app registers what it can do; a flow's App action block asks for it; the client app does it and answers.

PUT /v1/actions  [ {name: "pay", label: "Pay now", params: {...}, wait: true, outcomes: ["paid","failed"], timeoutSec: 120}, … ]

Caller in a flow reaches  "pay_1": {type: "app_action", action: "pay", params: {amount: "{amount}"}, x: {paid: …, failed: …, timeout: …}}
   engine ─► webhook action.requested {actionId, name: "pay", callId, params: {amount: "45000", phone: "+2557…"}, wait: true, timeoutAt}
   caller waits (music, "Thibitisha malipo kwenye simu yako")
   client app pushes M-Pesa … PIN entered …
   client app ─► POST /v1/calls/{callId}/action-result {actionId, outcome: "paid", data: {receipt: "QK7…"}}
   engine: the flow goes on through the "paid" exit; {action.pay_1.receipt} can be said

wait: false actions (send SMS, tag a contact) are fired and the call goes on at once.


1.10 Recordings, transcripts, summaries

FieldRecorded DescriptionWhen
validVoicemail trueAlways, when there are no errors (warnings are allowed)
errors[] / warnings[]{level, block, code, message}; codes in Part 4. Typical warning: EXIT_UNCONNECTED (the call ends there)
varsVariables thea flow uses thatreaches a call-outVoicemail must provideblock ({name},up to {amount}voicemailMaxSeconds)
textsLive parts EveryTalk fixedwith sentence;an agent, when the engineaccount's turns these into audio when a versionrecording.mode is storedLIVE_PARTS (default) and the department / block allows it. Callers hear the noticeText first
riskyBlocksCallers' spoken answers BlocksShort thatclips makeused thisfor versionspeech needrecognition; approvalnot (risky block types, and App actions registeredkept as risky)recordings

TheAfter answera recording: recording.ready webhook → GET /v1/calls/{id}/recording (MP3). If recording.transcribe is alwayson (default), a transcript and a short summary follow: 200summary.ready when→ GET /v1/calls/{id}/transcript. Recordings are encrypted at rest and deleted after the document could be read;account's validretentionDays says(default whether90).


1.11 Costs

Every call carries providerCost: what it wouldcost beRingmaster stored.in Whenphone theminutes and AI, line by line.

"providerCost": { "tzs": 68.61, "usd": 0.000334, "items": [
  { "provider": "AT", "tzs": 68.61, "lines": [
      { "item": "AT_OUTBOUND", "qty": 1, "unit": "MINUTE", "rate": 60, "currency": "TZS", "amount": 60, "source": "RATE" },
      { "item": "AT_SIP_TRUNK", "qty": 1, "unit": "MINUTE", "rate": 8.61, "currency": "TZS", "amount": 8.61, "source": "RATE" } ] },
  { "provider": "OPENAI", "usd": 0.000334, "lines": [
      { "item": "OPENAI_TRANSCRIBE", "qty": 0.0742, "unit": "MINUTE", "rate": 0.0045, "currency": "USD", "amount": 0.0003339, "source": "MEASURED", "model": "gpt-transcribe" } ] } ] }

The client app hasdecides nowhat AIto feature,charge eachits blockcustomers; thatthese usesare AIinputs, (AInot Agent, speech.match: "smart") is an error with code FEATURE_OFF.prices.


B.

1.12 Accounts

An account is one business. Its id is used in most paths. externalRef is the client app's own id for it.

B1. Create an accountFormats

Purpose: Add a business. It starts with sensible defaults (hours Mon–Fri 08–17, Sat 09–13; quiet hours 21:00–07:00; recording of live parts with a Kiswahili notice; limits; 90 days retention).

Endpoint: POST {base_url}/accounts

Access Level: 🔒 Protected · Bearer API key

Request JSON Sample:

{
  "externalRef": "pmk-chemicals",
  "defaultLanguage": "sw",
  "timezone": "Africa/Dar_es_Salaam"
}
(accounts),ref TZS
ParameterWhat TypeRequiredDescriptionValidationFormat
Phone numbersE.164 with +: +255745051250
IDsUUID strings; your own ids go in externalRef string Yes The(scripts, client app's id for the businessNot blank, max 120; unique within the client appagents)
defaultLanguageref / externalRef stringYour text id, up to 120 characters; letters, digits, -, _ recommended
Times in responses NoUTC ISO 8601 (2026-11-13T14:41:00.802627Z)
Times inside an account LanguageHH:mm callersin hearthe firstaccount's timezone (opening hours, quiet hours, campaign windows)
Languages sw (Kiswahili, default), en
timezoneLists stringGET /v1/calls pages with page (from 0) and size (default 50) → {calls, page, size, total}; other lists return everything
Money in costs No Forwith hours,2 quietdecimals, hours,USD "callwith meup atto 9"IANA zone; default Africa/Dar_es_Salaam7

Success Response: 201 Created, data = the account, same shape as B3.

Errors:

{ "success": false, "httpStatus": "409 CONFLICT", "message": "An account with this externalRef already exists",
  "code": "ACCOUNT_EXISTS", "action_time": "2026-11-13T19:57:58.985027739", "data": null }
{ "success": false, "httpStatus": "422 UNPROCESSABLE_CONTENT", "message": "Validation failed",
  "code": "VALIDATION_FAILED", "action_time": "2026-11-13T19:57:59.051669172", "data": { "externalRef": "must not be blank" } }
{ "success": false, "httpStatus": "400 BAD_REQUEST", "message": "Unknown timezone: Mars/Base",
  "code": "BAD_REQUEST", "action_time": "2026-11-13T19:57:59.009578709", "data": null }

B2. List accounts

Purpose: All accounts of the client app, or the one with a given externalRef.

Endpoint: GET {base_url}/accounts

Access Level: 🔒 Protected · Bearer API key

QueryTypeRequiredDescription
externalRefstringNoReturn only this account (a list of 0 or 1)

Success Response: data = array of accounts (B3 shape).


B3. Get an account

Endpoint: GET {base_url}/accounts/{accountId}

Access Level: 🔒 Protected · Bearer API key

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "200 OK",
  "message": "Account",
  "action": null,
  "action_time": "2026-11-13T19:57:58.939219236",
  "data": {
    "id": "adb72fac-66a2-471b-a437-035fe008197d",
    "externalRef": "softphone",
    "status": "ACTIVE",
    "defaultLanguage": "sw",
    "timezone": "Africa/Dar_es_Salaam",
    "hours": {
      "days": {
        "MONDAY": [ { "from": "08:00", "to": "17:00" } ],
        "TUESDAY": [ { "from": "08:00", "to": "17:00" } ],
        "WEDNESDAY": [ { "from": "08:00", "to": "17:00" } ],
        "THURSDAY": [ { "from": "08:00", "to": "17:00" } ],
        "FRIDAY": [ { "from": "08:00", "to": "17:00" } ],
        "SATURDAY": [ { "from": "09:00", "to": "13:00" } ]
      }
    },
    "quietHours": { "from": "21:00", "to": "07:00" },
    "recording": {
      "mode": "LIVE_PARTS",
      "noticeText": "Mazungumzo haya yanarekodiwa kwa ajili ya ubora wa huduma.",
      "noticeMediaId": null,
      "transcribe": true
    },
    "limits": {
      "maxCallMinutes": 15,
      "maxWaitSeconds": 300,
      "callerCallsPer10Min": 100,
      "otpPerDestinationPer10Min": 3,
      "voicemailMaxSeconds": 120,
      "maxBlockVisits": 25
    },
    "hasRequestSigningSecret": false,
    "retentionDays": 90,
    "businessName": "Ringmaster Lab",
    "aiDisclosure": null,
    "createdAt": "2026-11-12T05:20:55.204082Z"
  }
}
FieldDescription
statusACTIVE · SUSPENDED (every call to its numbers is refused)
hours.daysOpening hours per day (MONDAY…SUNDAY), several ranges allowed; a missing day = closed. Used by the Condition block (hours test) and Connect
quietHoursNo call-outs between from and to (may cross midnight)
recording.modeLIVE_PARTS (record talk with agents) · OFF (only voicemails)
recording.noticeText / noticeMediaIdWhat callers hear before a recorded part (text, or an uploaded audio file)
recording.transcribeMake a transcript and summary of each recording
limits.*Guards, see Part 1 §1.4
hasRequestSigningSecretA secret is set for signing Send request blocks' calls to the business's own systems (the secret itself is never returned)
retentionDaysRecordings and transcripts are deleted after this many days
businessNameSaid in the AI notice ("…huduma ya kidijitali ya Ringmaster Lab")
aiDisclosureOwn AI notice per language { "sw": "…", "en": "…" }; null = the engine's default sentence

Errors: 404 NOT_FOUND "Account not found" (unknown, or another client app's).


B4. Update an account

Purpose: Change any setting. Send only what changes; missing fields keep their value.

Endpoint: PATCH {base_url}/accounts/{accountId}

Access Level: 🔒 Protected · Bearer API key

Request JSON Sample:

{
  "businessName": "PMK Chemicals",
  "hours": { "days": { "MONDAY": [ { "from": "08:00", "to": "12:30" }, { "from": "14:00", "to": "17:00" } ] } },
  "quietHours": { "from": "20:00", "to": "08:00" },
  "recording": { "mode": "LIVE_PARTS", "noticeText": "Simu hii inarekodiwa.", "transcribe": true },
  "limits": { "maxCallMinutes": 20, "maxWaitSeconds": 300, "callerCallsPer10Min": 5,
              "otpPerDestinationPer10Min": 3, "voicemailMaxSeconds": 120, "maxBlockVisits": 25 },
  "retentionDays": 180,
  "aiDisclosure": { "sw": "Unazungumza na msaidizi wa kidijitali wa PMK.", "en": "You are talking to PMK's digital assistant." }
}
ParameterTypeDescriptionValidation
statusstringACTIVE · SUSPENDEDSuspending refuses new calls to its numbers and pauses its running call-outs; going back to ACTIVE does not restart them (the messages may be stale): resume or stop each one
defaultLanguagestringsw · en
timezonestringIANA zone400 Unknown timezone
hoursobjectReplaces all opening hoursHH:mm
quietHoursobject{from, to}HH:mm
recordingobjectReplaces the recording policymode = OFF / LIVE_PARTS
limitsobjectReplaces all six limits (send all six)whole numbers
requestSigningSecretstringSecret for Send request signatures; "" removes itstored encrypted
retentionDaysintegerDays to keep recordings1–3650
businessNamestring"" removes it
aiDisclosureobject{} goes back to the default sentence

Success Response: the updated account (B3 shape).


C. Numbers

A number moves through three states:

POOL ──(owner gives it to the client app)──► ALLOCATED ──assign──► ASSIGNED (to one account) ──release──► ALLOCATED
                                                  ▲  remove (NUMBERS feature) ◄── POOL      transfer: account → account

Only an assigned number with a pinned, runnable Incoming flow answers calls.

C1. List numbers

Endpoint: GET {base_url}/numbers

Access Level: 🔒 Protected · Bearer API key

QueryTypeRequiredDescription
statusstringNoALLOCATED (free, not on an account) · ASSIGNED. Anything else → 400 Invalid value for parameter: status

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "200 OK",
  "message": "Numbers",
  "action": null,
  "action_time": "2026-11-13T19:58:07.321779977",
  "data": [
    {
      "id": "8a8d9c13-bda2-4a18-8410-c5ec49ed2c66",
      "e164": "+255677000001",
      "status": "ASSIGNED",
      "accountId": "adb72fac-66a2-471b-a437-035fe008197d",
      "label": null,
      "incomingScriptVersionId": "e183c7f1-3137-4848-95c1-11a7e8bef17a",
      "verified": true,
      "verifiedAt": "2026-11-12T05:33:27.481181Z",
      "ringMode": "BROWSER"
    }
  ]
}
FieldDescription
statusALLOCATED · ASSIGNED
accountIdThe account it is on (null when free)
labelThe client app's name for it ("Mauzo", "Line 7")
incomingScriptVersionIdThe flow version callers hear; null = calls are refused
verified / verifiedAtA real call has reached the engine through the trunk on this number
ringModeWhere agents are rung: BROWSER · MOBILE · BROWSER_AND_MOBILE

C2. Add a number

Purpose: Bring a Tanzanian number the client app owns at the carrier into Ringmaster (it must be routed to the engine's trunk). Needs the NUMBERS feature.

Endpoint: POST {base_url}/numbers

Access Level: 🔒 Protected · Bearer API key · feature NUMBERS

Request JSON Sample:

{ "e164": "+255677000099", "type": "REGULAR", "label": "Mauzo", "by": "admin@business.co.tz" }
ParameterTypeRequiredDescriptionValidation
e164stringYesThe number+255 and 9 digits
typestringNoREGULAR (default) · TOLL_FREE
labelstringNoName for itmax 80
bystringNoWho did it in the client app (kept in the audit log)max 120

Success Response: 201 Created, data = the number (status ALLOCATED).

Errors:

{ "success": false, "httpStatus": "403 FORBIDDEN", "message": "Adding and removing numbers is not enabled for this client app",
  "code": "FEATURE_OFF", "action_time": "2026-11-13T19:58:07.349143236", "data": null }

400 "Only Tanzanian numbers (+255 and 9 digits) can be added" · 409 NUMBER_ALREADY_YOURS · 409 NUMBER_TAKEN (another client app has it).


C3. Remove a number

Purpose: Give a free number back to the pool. Needs the NUMBERS feature.

Endpoint: DELETE {base_url}/numbers/{numberId}

Access Level: 🔒 Protected · Bearer API key · feature NUMBERS

QueryTypeRequiredDescription
bystringNoWho did it (audit log)

Success Response: 200, message "Number removed", data null.

Errors: 409 NUMBER_ASSIGNED (release it first) · 409 NUMBER_BUSY (a call is in progress) · 409 NUMBER_IN_CALLOUT (a scheduled, running or paused call-out calls from it).


C4. Assign a number to an account

Endpoint: POST {base_url}/numbers/{numberId}/assign

Access Level: 🔒 Protected · Bearer API key

Request JSON Sample:

{ "accountId": "adb72fac-66a2-471b-a437-035fe008197d", "label": "Mapokezi" }
ParameterTypeRequiredDescription
accountIduuidYesThe account
labelstringNoName for the number

Success Response: the number with status: "ASSIGNED". Assigning again to the same account only changes the label.

Errors: 409 NUMBER_ASSIGNED "Number is assigned to another account; release it first" (use C6 instead).


C5. Release a number from its account

Purpose: Take the number off the account; it becomes free (ALLOCATED) and its flow is cleared.

Endpoint: POST {base_url}/numbers/{numberId}/release

Access Level: 🔒 Protected · Bearer API key

Success Response: the number with status: "ALLOCATED", accountId: null, incomingScriptVersionId: null.

Errors: 409 NUMBER_BUSY · 409 NUMBER_IN_CALLOUT.


C6. Transfer a number to another account

Purpose: Move an assigned number straight to another account of the same client app. The old account's flow is cleared, so pin the new account's flow next (C7).

Endpoint: POST {base_url}/numbers/{numberId}/transfer

Access Level: 🔒 Protected · Bearer API key

Request JSON Sample:

{ "accountId": "51f815ba-e9eb-4e29-9297-3c56033d32cd", "label": "Oda", "by": "admin@business.co.tz" }

Errors:

{ "success": false, "httpStatus": "409 CONFLICT", "message": "The number is already on this account",
  "code": "SAME_ACCOUNT", "action_time": "2026-11-13T19:58:07.424064672", "data": null }

409 NUMBER_NOT_ASSIGNED (assign it instead) · 409 ACCOUNT_NOT_ACTIVE · 409 NUMBER_BUSY · 409 NUMBER_IN_CALLOUT.


C7. Put a flow on a number

Purpose: Choose the flow version callers hear. Takes effect for the next call; calls in progress keep their version.

Endpoint: PUT {base_url}/numbers/{numberId}/script

Access Level: 🔒 Protected · Bearer API key

Request JSON Sample:

{ "scriptVersionId": "e183c7f1-3137-4848-95c1-11a7e8bef17a" }
ParameterTypeRequiredDescription
scriptVersionIduuid / nullYesA runnable version of an Incoming flow of the number's account (versionId from Part 2B); null takes the flow off (calls are refused)

Success Response: the number, message "Incoming script set".

Errors:

  • 400 "Assign the number to an account first"
  • 400 "Script version belongs to another account" · 400 "Numbers run Incoming scripts only"
  • 404 NOT_FOUND "Script version not found"
  • 409 VERSION_NOT_RUNNABLE "Only runnable versions can be put on a number" (approve it, Part 2B D7)

C8. Set where agents are rung

Endpoint: PUT {base_url}/numbers/{numberId}/ring-mode

Access Level: 🔒 Protected · Bearer API key

Request JSON Sample:

{ "ringMode": "BROWSER_AND_MOBILE" }
ParameterTypeRequiredDescription
ringModestringNoBROWSER (default; also when missing) · MOBILE (agents' mobile numbers) · BROWSER_AND_MOBILE

Success Response: the number, message "Ring mode set".

Errors: an unknown value → 400 "Request body is not valid JSON".