Part 2B — Call flows, media, knowledge base
← Contents · Previous: Part 2A — Setup · Next: Part 2C — Calls
A call flow (API name: script) is identified by its ref inside an account (orders, account-balance). Its
content lives in numbered versions that never change once stored. How to write the document itself (blocks,
settings, exits, messages, variables): SCRIPTS.md. Life of a flow: Part 1 §1.6.
All endpoints: 🔒 Protected · Bearer API key. {accountId} must be one of the client app's accounts (else 404).
D. Call flows (scripts)
D1. List flows
Purpose: Every flow of an account with all its versions (deleted flows are not listed).
Endpoint: GET {base_url}/accounts/{accountId}/scripts
Success Response: data = array of flows, sorted by ref, each as in D3.
D2. Create or rename a flow
Purpose: Make the flow (before its first version), or change its display name. Calling it again with the same
ref only renames.
Endpoint: PUT {base_url}/accounts/{accountId}/scripts/{ref}
Request JSON Sample:
{ "name": "Account balance", "direction": "INCOMING" }
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
{ref} (path) |
string | Yes | The client app's id for the flow | max 120; letters, digits, -, _ |
name |
string | Yes | Display name | not blank |
direction |
string | Yes | INCOMING (plays on a number) · OUTGOING (plays when a called person answers) |
fixed once created |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "200 OK",
"message": "Script saved",
"action": null,
"action_time": "2026-11-13T20:00:15.876328395",
"data": {
"id": "d3c063a6-1d76-415e-b623-c5769c1f6e69",
"ref": "docs-sample",
"name": "Account balance",
"direction": "INCOMING",
"versions": []
}
}
Errors: 409 DIRECTION_FIXED "A script keeps its direction; duplicate it to use the other direction".
A flow that was deleted after taking calls (archived) comes back with its old versions, which stay stopped.
D3. Get a flow
Endpoint: GET {base_url}/accounts/{accountId}/scripts/{ref}
Success Response JSON Sample:
{
"success": true,
"httpStatus": "200 OK",
"message": "Script",
"action": null,
"action_time": "2026-11-13T20:00:42.882898745",
"data": {
"id": "d3c063a6-1d76-415e-b623-c5769c1f6e69",
"ref": "docs-sample",
"name": "Account balance",
"direction": "INCOMING",
"versions": [
{
"versionId": "65a926f5-9fdc-49ed-95a7-93a70e562460",
"version": 1,
"runnable": true,
"testOnly": false,
"revoked": false,
"vars": [],
"texts": ["Bonyeza namba yako ya akaunti.", "Asante, kwaheri."],
"riskyBlocks": ["acct"],
"warnings": [],
"createdAt": "2026-11-13T17:00:15.991897Z"
},
{
"versionId": "15baa310-3997-4d14-a66e-a9c024f909ec",
"version": 2,
"runnable": false,
"testOnly": false,
"revoked": false,
"vars": [],
"texts": ["Bonyeza namba yako ya akaunti.", "Asante, kwaheri."],
"riskyBlocks": ["acct"],
"warnings": [],
"createdAt": "2026-11-13T17:00:42.855216Z"
}
]
}
}
| Field | Description |
|---|---|
versions[].versionId |
What numbers (PUT /v1/numbers/{id}/script) and call-outs (scriptVersionId) point at |
versions[].version |
The number the client app gave it |
runnable |
May run: be put on a number or used for calls out |
testOnly |
For test calls only; never runnable |
revoked |
Stopped for good |
vars |
Variables a call-out must give each person (name, amount); system variables are not listed |
texts |
Fixed sentences, already turned into audio. Sentences with variables (Akaunti {input.account}…) are spoken during the call |
riskyBlocks |
Blocks that make the version need approval (D7) |
warnings |
Only in the answer to a push (D5); empty when read later |
Errors: 404 NOT_FOUND "Script not found: docs-sample".
D4. Delete a flow
Purpose: Remove a flow. Refused while anything uses it. A flow that never ran is erased; a flow that ran calls is
archived: hidden from lists, all its versions stopped, its call history kept. Creating the same ref again
(D2) brings it back.
Endpoint: DELETE {base_url}/accounts/{accountId}/scripts/{ref}
Success Response JSON Sample:
{
"success": true,
"httpStatus": "200 OK",
"message": "Script deleted",
"action": null,
"action_time": "2026-11-13T20:00:43.15694553",
"data": { "ref": "docs-sample", "outcome": "DELETED" }
}
| Field | Description |
|---|---|
outcome |
DELETED (never ran: gone) · ARCHIVED (ran calls: hidden, history kept) |
Error Response JSON Sample (still in use):
{
"success": false,
"httpStatus": "409 CONFLICT",
"message": "The script is still in use: move these to another script first",
"code": "SCRIPT_IN_USE",
"action_time": "2026-11-13T18:09:38.582694597",
"data": {
"numbers": ["+255677000003"],
"callouts": ["Callback +255745051250"],
"scripts": ["account v4"]
}
}
data key |
Means |
|---|---|
numbers |
Numbers playing one of its versions → put another flow on them (2A C7) |
callouts |
Call-outs not finished (draft, scheduled, running, paused) using it → cancel or let them finish |
scripts |
Live versions of other flows that transfer to it or schedule callbacks with it → push new versions without that |
D5. Push a version
Purpose: Store a new version. The engine checks it first (SCRIPTS.md §9): errors → nothing stored. Fixed sentences are turned into audio straight away, and an AI Agent's knowledge is prepared in the background.
Endpoint: POST {base_url}/accounts/{accountId}/scripts/{ref}/versions
Request JSON Sample:
{
"version": 2,
"runnable": false,
"definition": {
"direction": "INCOMING",
"start": { "call": "acct" },
"blocks": {
"acct": { "type": "collect", "msg": { "src": "text", "text": "Bonyeza namba yako ya akaunti." },
"saveAs": "account", "minDigits": 4, "maxDigits": 4,
"x": { "ok": "read", "failed": "bye", "later": "bye" } },
"read": { "type": "say", "msg": { "src": "text", "text": "Akaunti {input.account} ina salio la shilingi elfu kumi." },
"x": { "next": "bye" } },
"bye": { "type": "goodbye", "msg": { "src": "text", "text": "Asante, kwaheri." } }
}
}
}
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
version |
integer | Yes | The version number; the client app counts | ≥ 1, unique per flow |
definition |
object | Yes | The flow document | must pass the check; direction must match the flow; a layout key is dropped |
runnable |
boolean | No | true = may run at once. Push false when the version has riskyBlocks and waits for approval |
default false |
testOnly |
boolean | No | For test calls only (never runnable) | default false |
Success Response JSON Sample (201 Created):
{
"success": true,
"httpStatus": "201 CREATED",
"message": "Version stored",
"action": null,
"action_time": "2026-11-13T20:00:16.018139374",
"data": {
"versionId": "65a926f5-9fdc-49ed-95a7-93a70e562460",
"version": 1,
"runnable": true,
"testOnly": false,
"revoked": false,
"vars": [],
"texts": ["Bonyeza namba yako ya akaunti.", "Asante, kwaheri."],
"riskyBlocks": ["acct"],
"warnings": [
{ "level": "WARNING", "block": "acct", "code": "EXIT_UNCONNECTED", "message": "Exit 'later' is not connected: the call ends there" }
],
"createdAt": "2026-11-13T17:00:15.991897467Z"
}
}
Sending the same version again with the same document (e.g. a retry after a timeout) is safe: 200 OK,
message "Version already stored", same data. Secrets in Send request blocks are encrypted before storing and do not
change the comparison.
Error Response JSON Samples:
{
"success": false,
"httpStatus": "422 UNPROCESSABLE_CONTENT",
"message": "Script version is not valid",
"code": "VALIDATION_FAILED",
"action_time": "2026-11-13T20:00:16.178937467",
"data": [
{ "level": "ERROR", "block": "a", "code": "EXIT_TARGET_MISSING", "message": "Exit 'next' points to a missing block: zz" }
]
}
{
"success": false,
"httpStatus": "409 CONFLICT",
"message": "Version 1 already exists with a different definition; versions are immutable",
"code": "VERSION_EXISTS",
"action_time": "2026-11-13T20:00:16.123223371",
"data": null
}
Also 400 "definition must be a JSON object" · 404 "Script not found" (create it with D2 first) ·
403 FEATURE_OFF "AI is not enabled for this client app; these blocks use it: ai" (an AI Agent block or smart speech
matching without the AI feature). Check codes: Part 4.
D6. Get a version (with its blocks)
Purpose: Read a version back: to show it, edit it into the next version, or see what a call ran.
Endpoint: GET {base_url}/accounts/{accountId}/scripts/{ref}/versions/{version}
Success Response JSON Sample:
{
"success": true,
"httpStatus": "200 OK",
"message": "Script version",
"action": null,
"action_time": "2026-11-13T20:00:42.918636988",
"data": {
"ref": "docs-sample",
"name": "Account balance",
"direction": "INCOMING",
"version": {
"versionId": "15baa310-3997-4d14-a66e-a9c024f909ec",
"version": 2,
"runnable": false,
"testOnly": false,
"revoked": false,
"vars": [],
"texts": ["Bonyeza namba yako ya akaunti.", "Asante, kwaheri."],
"riskyBlocks": ["acct"],
"warnings": [],
"createdAt": "2026-11-13T17:00:42.855216Z"
},
"definition": {
"direction": "INCOMING",
"start": { "call": "acct" },
"blocks": {
"acct": { "type": "collect", "msg": { "src": "text", "text": "Bonyeza namba yako ya akaunti." },
"saveAs": "account", "minDigits": 4, "maxDigits": 4,
"x": { "ok": "read", "later": "bye", "failed": "bye" } },
"read": { "type": "say", "msg": { "src": "text", "text": "Akaunti {input.account} ina salio la shilingi elfu kumi." },
"x": { "next": "bye" } },
"bye": { "type": "goodbye", "msg": { "src": "text", "text": "Asante, kwaheri." } }
}
}
}
}
| Field | Description |
|---|---|
version |
Same as in D3 |
definition |
The stored document. Keys come back sorted. Secrets of Send request blocks (auth.token, auth.password, auth.value) come back as "(hidden)" — keep the real ones in the client app if the editor must re-push them |
Errors: 404 "Version not found: 9".
D7. Approve a version (make it runnable)
Purpose: After the client app's reviewer approves a version pushed with runnable: false. Call-outs that were
waiting for it start by themselves.
Endpoint: POST {base_url}/accounts/{accountId}/scripts/{ref}/versions/{version}/runnable
Success Response: 200, message "Version is runnable", data = the version (runnable: true).
Errors:
{
"success": false,
"httpStatus": "409 CONFLICT",
"message": "A revoked version cannot become runnable",
"code": "VERSION_REVOKED",
"action_time": "2026-11-13T20:00:43.054366231",
"data": null
}
D8. Stop a version (revoke)
Purpose: Stop a version for good (it was wrong, or the business rejected it). It can never run again.
Endpoint: POST {base_url}/accounts/{accountId}/scripts/{ref}/versions/{version}/revoke
Success Response: 200, message "Version revoked", data = the version (revoked: true, runnable: false).
Revoking twice is harmless.
Move things off it first. A number still playing a revoked version refuses calls (callers hear that the service is unavailable), and running call-outs using it are paused with
pauseReason: "VERSION_REVOKED".
D9. Block stats of a version
Purpose: Where callers go and where they get stuck, per block — for the flow editor's heat map.
Endpoint: GET {base_url}/accounts/{accountId}/scripts/{ref}/versions/{version}/stats
| Query | Type | Required | Description | Default |
|---|---|---|---|---|
from |
date | No | First day, YYYY-MM-DD (account's time zone) |
6 days before to |
to |
date | No | Last day | today |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "200 OK",
"message": "Block stats",
"action": null,
"action_time": "2026-11-13T20:01:13.597312525",
"data": [
{ "key": "acct", "values": { "visits": 5, "hangups": 0, "noInput": 0, "retries": 2,
"reqOk": 0, "reqFail": 0, "reqTimeout": 0, "reqMsP50": null, "gaveUp": 0 } },
{ "key": "when", "values": { "visits": 4, "hangups": 0, "noInput": 0, "retries": 1,
"reqOk": 0, "reqFail": 0, "reqTimeout": 0, "reqMsP50": null, "gaveUp": 0 } }
]
}
| Field | Description |
|---|---|
key |
Block id |
visits |
Times a call entered the block |
hangups |
Callers who hung up in it |
noInput |
Times nobody answered its question |
retries |
Questions asked again (wrong or no answer) |
reqOk / reqFail / reqTimeout / reqMsP50 |
Send request blocks: results and median time in ms |
gaveUp |
Waiting rooms the caller left after the give-up time |
Stats are rolled up a few minutes after calls end.
E. Media (audio files)
Recorded greetings, menu prompts and hold music. Upload once, then use the id in a flow message:
{"src": "upload", "media": "<mediaId>"} (a waiting room's music takes the id directly).
E1. Upload audio
Endpoint: POST {base_url}/accounts/{accountId}/media · multipart/form-data
| Part / query | Type | Required | Description | Validation |
|---|---|---|---|---|
file (part) |
file | Yes | MP3, WAV, OGG, M4A… | an audio file; ≤ 10 MB; ≤ 5 minutes |
name (query) |
string | No | Display name | default: the file name |
lang (query) |
string | No | sw · en (for the library filter) |
curl -X POST "$BASE/accounts/$ACCOUNT/media?name=Salamu%20ya%20duka&lang=sw" \
-H "Authorization: Bearer $RINGMASTER_KEY" -F "file=@salamu.mp3"
Success Response: 202 Accepted, message "Audio received; processing", data = the media item with status: "PENDING". The engine converts it for the
phone line in the background, then sends media.ready ({mediaId, durationMs}) or media.failed
({mediaId, error}) — Part 3. Use the id in a flow only once it is READY.
Errors:
{
"success": false,
"httpStatus": "422 UNPROCESSABLE_CONTENT",
"message": "Upload rejected",
"code": "VALIDATION_FAILED",
"action_time": "2026-11-13T20:01:13.708314472",
"data": [ { "field": "file", "message": "Not an audio file" } ]
}
Other messages: "File is larger than 10 MB" · "Only audio files are accepted" · "Audio is longer than 5 minutes".
409 STORAGE_QUOTA — the account has used its 500 MB of audio.
E2. List audio
Endpoint: GET {base_url}/accounts/{accountId}/media
Success Response JSON Sample:
{
"success": true,
"httpStatus": "200 OK",
"message": "Audio library",
"action": null,
"action_time": "2026-11-13T20:01:13.551808448",
"data": [
{
"id": "eebb855e-2a03-4af7-a8c8-301f85877308",
"name": "Muziki wa kusubiri",
"kind": "UPLOAD",
"status": "READY",
"durationMs": 24192,
"lang": "sw",
"error": null,
"previewUrl": "https://<ringmaster-host>/media/eebb855e-…/eda4fdcb….mp3?t=f702ec7bb40e96291e689462aa8319c9",
"createdAt": "2026-11-13T09:46:03.541625Z"
}
]
}
| Field | Description |
|---|---|
kind |
UPLOAD (uploaded) · RECORDING (recorded in the editor) · SYSTEM (engine's own: beeps, default hold music) |
status |
PENDING → PROCESSING → READY · FAILED (see error) |
durationMs |
Length once ready |
previewUrl |
Signed link to play it in a browser (<audio src>), no API key needed; it expires — read it again rather than storing it |
E3. Get one audio item
Endpoint: GET {base_url}/media/{mediaId}
Success Response: one item as in E2. Errors: 404 "Audio not found".
E4. Delete audio
Endpoint: DELETE {base_url}/media/{mediaId}
Success Response: 200, message "Audio deleted", data null.
Errors: 404 "Audio not found" · 409 MEDIA_IN_USE "Audio is used by live script versions", data: {"usedBy": ["orders v3", "reception v2"]}
— push versions without it (or revoke those) first.
F. Knowledge base (for AI Agents)
Documents an AI Agent may answer from. Upload them, then list their ids in the AI Agent block's kb.docs
(SCRIPTS.md). Each flow version builds its own searchable copy when it is stored, so a version keeps
answering the same way even if documents change later; push a new version to use new documents.
F1. Upload a document
Endpoint: POST {base_url}/accounts/{accountId}/kb-docs · multipart/form-data
| Part | Type | Required | Description | Validation |
|---|---|---|---|---|
file |
file | Yes | .txt, .md or .pdf with text |
≤ 10 MB; text is cut at 200,000 characters; scanned PDFs (images) have no text |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "201 CREATED",
"message": "Document stored",
"action": null,
"action_time": "2026-11-13T20:05:00.000000000",
"data": { "id": "7c2e…", "name": "bei_2026.pdf", "contentType": "application/pdf", "chars": 18420,
"status": "READY", "createdAt": "2026-11-13T17:05:00Z" }
}
Errors (400 BAD_REQUEST): "A file is required" · "Documents are limited to 10 MB" · "Upload a .txt, .md or .pdf document" ·
"No text found in this document (scanned PDFs are not read)".
F2. List documents
Endpoint: GET {base_url}/accounts/{accountId}/kb-docs
Success Response: data = array of documents as in F1.
F3. Delete a document
Endpoint: DELETE {base_url}/accounts/{accountId}/kb-docs/{documentId}
Success Response: 200, message "Document removed", data null. Versions already stored keep their copy of its
text. Errors: 404 "Document not found".