Part 2A — Setup: basics, accounts, numbers
← Contents · Previous: Part 1 · Next: Part 2B — Call flows
Every endpoint here needs Authorization: Bearer <api key> (🔒). The envelope and error codes are on the
contents page.
A. Basics
A1. Ping
Purpose: Check the key works and see which features the client app has.
Endpoint: GET {base_url}/ping
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"]
}
}
| Field | Description |
|---|---|
clientAppId |
The client app this key belongs to |
name |
Its name at the Ringmaster owner |
features |
What it may do: INBOUND, CALLOUTS, OTP, AI, NUMBERS (Part 1 §1.2) |
Error Response JSON Sample (no or wrong key):
{
"success": false,
"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 suspended the app), 429 RATE_LIMITED.
A2. Script schema
Purpose: The block catalogue a flow editor needs: every block type, its group, what it does, its exits, whether it needs approval; the rules; the message shape; the variables a flow can use; an example flow.
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": "Ask the caller to pick an option by key or, when speech is on, by voice …",
"exits": ["<option>", "noInput", "notUnderstood", "later"], "needsApproval": false },
{ "type": "collect", "group": "Ask", "description": "Collect a number (account, PIN, amount) into {input.<name>}.",
"exits": ["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." } }
}
}
}
}
| Field | Description |
|---|---|
blocks[].exits |
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[].needsApproval |
A risky block: a version using it should be approved before it runs. The engine reports these in riskyBlocks; the client app pushes such versions with runnable: false and approves them later (Part 2B, D7) |
systemVariables |
Always available in texts as {name}: {caller}, {date}, {time}, {lang}, {key} (last key), {heard} (last speech), {number} (number called), {callId}, {answeredBy} (outgoing, with answering-machine detection: person / machine) |
The full meaning of every block setting is in SCRIPTS.md. The schema is generated from the engine's
code, so it always matches what the engine accepts; cache it per schemaVersion.
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" } }
}
}
}
| Parameter | Type | Required | Description |
|---|---|---|---|
definition |
object | Yes | The flow document (SCRIPTS.md) |
accountId |
uuid | No | With it, references are checked too: departments, media, the flows a Transfer or Schedule block names |
Success Response JSON Sample (the check ran; this flow has an error):
{
"success": true,
"httpStatus": "200 OK",
"message": "Validation result",
"action": null,
"action_time": "2026-11-13T18:06:38.004985164",
"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": []
}
}
| Field | Description |
|---|---|
valid |
true 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) |
vars |
Variables the flow uses that a call-out must provide ({name}, {amount}) |
texts |
Every fixed sentence; the engine turns these into audio when a version is stored |
riskyBlocks |
Blocks that make this version need approval (risky block types, and App actions registered as risky) |
The answer is always 200 when the document could be read; valid says whether it would be stored. When the client
app has no AI feature, each block that uses AI (AI Agent, speech.match: "smart") is an error with code FEATURE_OFF.
B. 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 account
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"
}
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
externalRef |
string | Yes | The client app's id for the business | Not blank, max 120; unique within the client app |
defaultLanguage |
string | No | Language callers hear first | sw (default), en |
timezone |
string | No | For hours, quiet hours, "call me at 9" | IANA zone; default Africa/Dar_es_Salaam |
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
| Query | Type | Required | Description |
|---|---|---|---|
externalRef |
string | No | Return 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"
}
}
| Field | Description |
|---|---|
status |
ACTIVE · SUSPENDED (every call to its numbers is refused) |
hours.days |
Opening hours per day (MONDAY…SUNDAY), several ranges allowed; a missing day = closed. Used by the Condition block (hours test) and Connect |
quietHours |
No call-outs between from and to (may cross midnight) |
recording.mode |
LIVE_PARTS (record talk with agents) · OFF (only voicemails) |
recording.noticeText / noticeMediaId |
What callers hear before a recorded part (text, or an uploaded audio file) |
recording.transcribe |
Make a transcript and summary of each recording |
limits.* |
Guards, see Part 1 §1.4 |
hasRequestSigningSecret |
A secret is set for signing Send request blocks' calls to the business's own systems (the secret itself is never returned) |
retentionDays |
Recordings and transcripts are deleted after this many days |
businessName |
Said in the AI notice ("…huduma ya kidijitali ya Ringmaster Lab") |
aiDisclosure |
Own 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." }
}
| Parameter | Type | Description | Validation |
|---|---|---|---|
status |
string | ACTIVE · SUSPENDED |
Suspending 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 |
defaultLanguage |
string | sw · en |
|
timezone |
string | IANA zone | 400 Unknown timezone |
hours |
object | Replaces all opening hours | HH:mm |
quietHours |
object | {from, to} |
HH:mm |
recording |
object | Replaces the recording policy | mode = OFF / LIVE_PARTS |
limits |
object | Replaces all six limits (send all six) | whole numbers |
requestSigningSecret |
string | Secret for Send request signatures; "" removes it |
stored encrypted |
retentionDays |
integer | Days to keep recordings | 1–3650 |
businessName |
string | "" removes it |
|
aiDisclosure |
object | {} 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
| Query | Type | Required | Description |
|---|---|---|---|
status |
string | No | ALLOCATED (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"
}
]
}
| Field | Description |
|---|---|
status |
ALLOCATED · ASSIGNED |
accountId |
The account it is on (null when free) |
label |
The client app's name for it ("Mauzo", "Line 7") |
incomingScriptVersionId |
The flow version callers hear; null = calls are refused |
verified / verifiedAt |
A real call has reached the engine through the trunk on this number |
ringMode |
Where 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" }
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
e164 |
string | Yes | The number | +255 and 9 digits |
type |
string | No | REGULAR (default) · TOLL_FREE |
|
label |
string | No | Name for it | max 80 |
by |
string | No | Who 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
| Query | Type | Required | Description |
|---|---|---|---|
by |
string | No | Who 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" }
| Parameter | Type | Required | Description |
|---|---|---|---|
accountId |
uuid | Yes | The account |
label |
string | No | Name 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" }
| Parameter | Type | Required | Description |
|---|---|---|---|
scriptVersionId |
uuid / null | Yes | A 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" }
| Parameter | Type | Required | Description |
|---|---|---|---|
ringMode |
string | No | BROWSER (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".