Skip to main content

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".