Ringmaster Engine API Documentation Introduction Author : Josh S. Sakweli, Backend Lead Team Last Updated : 2026-11-13 Version : v1.0 Base URL : https:///v1 (local: http://localhost:8085/v1 ) Short Description : Ringmaster is an engine for intelligent call centres . Services build their calling features on it (they are its client apps ): the engine does the telephony, the call logic, the AI and the agents; the service owns its users, its screens and its business rules. A business on such a service (an account ) gets phone numbers; each number plays a call flow (a script of blocks: greet, menu, collect a number, voicemail, AI agent, connect to a person…). Ringmaster also calls people (reminders, campaigns, callbacks, OTP codes), rings the business's agents in their browser, records and transcribes calls, and tells the client app what happened through signed webhooks. This book is for developers of a client app : every endpoint, every event the engine sends, every value. Hints : Every /v1 request carries the client app's API key: Authorization: Bearer . Keep it on your server; never ship it to a browser or phone. Ringmaster knows nothing about the client app's users, contacts, money or messages. It stores what it needs to ring and play: account settings, numbers, call flows, agents (a ref and a name), calls and their reports. Phone numbers are always E.164 ( +255745051250 ). Times in responses are UTC ISO 8601 ( 2026-11-13T14:41:00Z ); times inside an account (opening hours, quiet hours, "call me at 9") are in the account's time zone (default Africa/Dar_es_Salaam ). Refusals you should handle in code come with a stable code ( SCRIPT_IN_USE , VERSION_NOT_RUNNABLE …); switch on the code, show the message. Full list in Part 4 . How a call flow is written (all blocks, their settings and exits) is in Part 5 ; how to build the editor, calls, reports and live screens on top of the API is in Part 6 . A working sample client to try everything by hand: the demo console ( console/ , python3 console/serve.py ); its "API calls" bar shows the exact request for each click. Contents Page What's in it Part 1 — How Ringmaster works Read first: the objects, a call step by step, call-outs, flows and versions, agents, webhooks, app actions, recordings, costs, formats Part 2A — Setup: basics, accounts, numbers ping, script schema and check · accounts · numbers Part 2B — Call flows, media, knowledge scripts and versions · audio files · knowledge base for the AI Part 2C — Calls, call-outs, OTP, reports call history, timeline, recording, transcript · quick calls and call-outs · OTP · stats and exports Part 2D — Agents and live calls departments, agents, the browser desk · accept, hold, transfer, hang up Part 2E — App actions and opt-outs registering the client app's actions and answering them · the do-not-call list Part 3 — What the engine sends you webhooks: envelope, signature, retries, every event · agent desk signals Part 4 — Reference every status and value · every error code · limits and defaults · glossary Part 5 — Call flow reference the flow document · messages · variables · all 18 blocks with every setting and exit · Waiting Room · AI Agent · check codes and fixes Part 6 — Building the screens flow editor · calls page and call log · reports · agent desk and queue · which webhook updates what · the don't list Appendix — Not for client apps engine-internal routes · what the Ringmaster owner sets up with ringmaster-admin Standard Response Format Every response uses the Globe envelope. Success { "success": true, "httpStatus": "OK", "message": "Script", "action": null, "action_time": "2026-11-13T17:44:40.645991261", "data": { } } Error { "success": false, "httpStatus": "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"] } } Field Type Description success boolean true for success, false for errors httpStatus string HTTP status, e.g. "200 OK" , "409 CONFLICT" message string Human-readable result; safe to show code string Errors only: stable machine code to switch on ( NOT_FOUND , VALIDATION_FAILED , SCRIPT_IN_USE …) action_time string Server time of the response (local, no zone) data any The payload; on errors, details (field messages, what is in the way) or null HTTP code When 400 BAD_REQUEST Body is not valid JSON, a query parameter is missing or not the right type, a value is not allowed 401 UNAUTHORIZED Missing or wrong API key 403 CLIENT_SUSPENDED , FEATURE_OFF , CAN_CALL_REFUSED The app is suspended, the feature is not enabled for it, or its can-call check said no 404 NOT_FOUND No such object, or it belongs to another client app 409 a specific code A rule says no (in use, wrong state, already answered); data says what is in the way 422 VALIDATION_FAILED Field errors in data ( {"field": "message"} ), or a script's error list 429 RATE_LIMITED More requests per second than the app's limit 500 INTERNAL_ERROR A bug on our side; the message is generic HTTP Method Badge Standards GET - GET - read only POST - POST - create / act PUT - PUT - create or replace PATCH - PATCH - change some fields DELETE - DELETE - remove Every endpoint at a glance Group Method Path Page Basics GET /v1/ping 2A GET /v1/scripts/schema 2A POST /v1/scripts/validate 2A Accounts POST · GET /v1/accounts 2A GET · PATCH /v1/accounts/{accountId} 2A Numbers GET · POST /v1/numbers 2A DELETE /v1/numbers/{numberId} 2A POST /v1/numbers/{numberId}/assign · /release · /transfer 2A PUT /v1/numbers/{numberId}/script · /ring-mode 2A Scripts GET /v1/accounts/{accountId}/scripts 2B PUT · GET · DELETE /v1/accounts/{accountId}/scripts/{ref} 2B POST /v1/accounts/{accountId}/scripts/{ref}/versions 2B GET /v1/accounts/{accountId}/scripts/{ref}/versions/{version} 2B POST …/versions/{version}/runnable · …/versions/{version}/revoke 2B GET …/versions/{version}/stats 2B Media POST · GET /v1/accounts/{accountId}/media 2B GET · DELETE /v1/media/{mediaId} 2B Knowledge POST · GET /v1/accounts/{accountId}/kb-docs 2B DELETE /v1/accounts/{accountId}/kb-docs/{documentId} 2B Calls GET /v1/calls · /v1/calls/{callId} · /timeline · /recording · /transcript 2C GET /v1/calls/export.csv 2C POST /v1/calls (quick call) · /v1/calls/agent 2C Call-outs POST · GET /v1/callouts 2C GET /v1/callouts/{id} · /recipients · /stats · /export.csv 2C POST /v1/callouts/{id}/recipients · /v1/callouts/{id}/{action} 2C OTP POST /v1/otp 2C Stats GET /v1/accounts/{accountId}/stats 2C Agents GET /v1/accounts/{accountId}/agents · /agents/stats 2D PUT · DELETE /v1/accounts/{accountId}/agents/{ref} 2D PUT · POST · POST …/agents/{ref}/status · /ready · /lobby-token 2D Departments POST · GET /v1/accounts/{accountId}/departments 2D PUT · DELETE …/departments/{departmentId} 2D PUT · GET …/departments/{departmentId}/members · /queue 2D Live calls POST /v1/calls/{callId}/accept · /decline · /hold · /unhold · /transfer · /hangup 2D App actions PUT · GET /v1/actions 2E POST /v1/calls/{callId}/action-result 2E Opt-outs POST · GET /v1/accounts/{accountId}/opt-outs 2E DELETE /v1/accounts/{accountId}/opt-outs/{e164} 2E Part 1 — How Ringmaster works ← Contents · Next: Part 2A — Setup Read this page before the endpoints. Ringmaster is the engine of 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 what the client app must do at each step. 1.1 Who's who Who What they are Example Ringmaster owner Runs the engine; creates client apps, buys phone numbers, gives numbers to client apps ( ringmaster-admin , see Appendix ) QBIT SPARK ops Client app A product that uses the engine through this API. Has one API key, a webhook URL and a set of features a messaging or business platform Account One business inside a client app. Everything (numbers, flows, calls, agents) belongs to an account PMK Chemicals, Kliniki ya Afya Bora Number A 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 versions orders v3 Agent A person who answers calls in the browser (or on a mobile). Ringmaster keeps only a ref and a name asha Department A group of agents a flow can send callers to Mapokezi, Uhasibu Caller / person called Anyone 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: Item What it is Where it's used API key vk_live_… , shown once Every request: Authorization: Bearer Webhook URL The client app's HTTPS endpoint, e.g. https://app.example.co.tz/hooks/ringmaster The engine POSTs events there ( Part 3 ) Webhook secret Shown once; signs every webhook The client app checks each event's signature Features What the app may do See below Rate limit Requests per second for the app (fixed one-second window) Over it → 429 RATE_LIMITED Feature Allows INBOUND Answering incoming calls. Without it every incoming call is refused ( REJECTED , FEATURE_OFF ) CALLOUTS Calling people: quick calls and call-outs ( 403 FEATURE_OFF without it); callers booking a callback (refused without it) OTP Voice OTP calls ( POST /v1/otp ); without it → 403 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 and removing numbers through the API; without it → 403 FEATURE_OFF Features are checked when something is made and again when it runs : if the owner takes 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 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 404 NOT_FOUND . 1.3 Setting up 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} From step 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 say "Karibu PMK Chemicals…" → menu "Bonyeza 1 kuagiza…" → (caller presses 1 or says "oda") │ every step is written to the call's timeline (BLOCK_ENTER, SAY, KEYS, SPEECH, …) │ call.block webhooks for the steps clients care about (menu choice, App action, …) ▼ … 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 What the caller hears is 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 agent) → ENDED . After it ends, result says how it went ( HANDLED_BY_SCRIPT , HANDLED_BY_AGENT , HANDLED_BY_AI , VOICEMAIL , MISSED , NO_ANSWER , BUSY , DECLINED …), endReason why it ended and endedBy who ended it. All values: Part 4 . Guards that protect every account (account limits , changeable with PATCH /v1/accounts/{id} ): Limit Default What happens maxCallMinutes 15 The call is ended ( MAX_CALL_LENGTH ) maxWaitSeconds 300 Longest wait in a waiting room, whatever the flow says callerCallsPer10Min 5 The same caller calling more often is refused ( REJECTED , CALLER_LIMIT ) otpPerDestinationPer10Min 3 More OTP calls to one number → refused voicemailMaxSeconds 120 Longest voicemail maxBlockVisits 25 Blocks run without any input from the caller before the call is ended ( LOOP_GUARD ) — stops endless loops 1.5 Calling people (call-outs) The engine places calls for an account and plays an Outgoing flow when the person answers. Way Endpoint Use Quick call POST /v1/calls "Your order is ready" to 1–1000 people, now. Created and started in one request Broadcast POST /v1/callouts kind: BROADCAST The same, but built in steps (create → add people → start), optionally at startsAt Campaign POST /v1/callouts kind: CAMPAIGN Calling a list over days: a daily window ( windowFrom / windowTo ), week days, pace per minute, up to 5 tries, a fallback App action (e.g. SMS) after the last failed try Scheduled callback made by a caller during a call (Schedule call block or AI schedule_call ) "Nipigie baada ya saa moja": a one-person call-out with ref schedule:: ; the client app sees it in GET /v1/callouts and the call.scheduled webhook OTP POST /v1/otp Read a code to a number Each person on a call-out is a recipient with their own vars (e.g. {"name": "Asha", "amount": "45000"} ) that the flow says as {name} , {amount} . Rules the engine applies to every call-out (the client app doesn'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 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, ".") { "id": "5b0c7f8e-…", "type": "call.ended", "createdAt": "2026-11-13T16:44:04Z", "data": { … } } 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 Recorded When Voicemail Always, when a flow reaches a Voicemail block (up to voicemailMaxSeconds ) Live parts Talk with an agent, when the account's recording.mode is LIVE_PARTS (default) and the department / block allows it. Callers hear the noticeText first Callers' spoken answers Short clips used for speech recognition; not kept as recordings After a recording: recording.ready webhook → GET /v1/calls/{id}/recording (MP3). If recording.transcribe is on (default), a transcript and a short summary follow: summary.ready → GET /v1/calls/{id}/transcript . Recordings are encrypted at rest and deleted after the account's retentionDays (default 90). 1.11 Costs Every call carries providerCost : what it cost Ringmaster in phone minutes 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 decides what to charge its customers; these are inputs, not prices. 1.12 Formats What Format Phone numbers E.164 with + : +255745051250 IDs UUID strings; your own ids go in externalRef (accounts), ref (scripts, agents) ref / externalRef Your text id, up to 120 characters; letters, digits, - , _ recommended Times in responses UTC ISO 8601 ( 2026-11-13T14:41:00.802627Z ) Times inside an account HH:mm in the account's timezone (opening hours, quiet hours, campaign windows) Languages sw (Kiswahili, default), en Lists GET /v1/calls pages with page (from 0) and size (default 50) → {calls, page, size, total} ; other lists return everything Money in costs TZS with 2 decimals, USD with up to 7 Part 2 — Setups, flows, calls, agents, Part 2A — Setup: basics, accounts, numbers ← Contents · Next: Part 2A — Setup Read this page before the endpoints. Ringmaster is the engine of 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 what the client app must do at each step. 1.1 Who's who Who What they are Example Ringmaster owner Runs the engine; creates client apps, buys phone numbers, gives numbers to client apps ( ringmaster-admin , see Appendix ) QBIT SPARK ops Client app A product that uses the engine through this API. Has one API key, a webhook URL and a set of features a messaging or business platform Account One business inside a client app. Everything (numbers, flows, calls, agents) belongs to an account PMK Chemicals, Kliniki ya Afya Bora Number A 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 versions orders v3 Agent A person who answers calls in the browser (or on a mobile). Ringmaster keeps only a ref and a name asha Department A group of agents a flow can send callers to Mapokezi, Uhasibu Caller / person called Anyone 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: Item What it is Where it's used API key vk_live_… , shown once Every request: Authorization: Bearer Webhook URL The client app's HTTPS endpoint, e.g. https://app.example.co.tz/hooks/ringmaster The engine POSTs events there ( Part 3 ) Webhook secret Shown once; signs every webhook The client app checks each event's signature Features What the app may do See below Rate limit Requests per second for the app (fixed one-second window) Over it → 429 RATE_LIMITED Feature Allows INBOUND Answering incoming calls. Without it every incoming call is refused ( REJECTED , FEATURE_OFF ) CALLOUTS Calling people: quick calls and call-outs ( 403 FEATURE_OFF without it); callers booking a callback (refused without it) OTP Voice OTP calls ( POST /v1/otp ); without it → 403 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 and removing numbers through the API; without it → 403 FEATURE_OFF Features are checked when something is made and again when it runs : if the owner takes 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 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 404 NOT_FOUND . 1.3 Setting up 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} From step 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 say "Karibu PMK Chemicals…" → menu "Bonyeza 1 kuagiza…" → (caller presses 1 or says "oda") │ every step is written to the call's timeline (BLOCK_ENTER, SAY, KEYS, SPEECH, …) │ call.block webhooks for the steps clients care about (menu choice, App action, …) ▼ … 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 What the caller hears is 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 agent) → ENDED . After it ends, result says how it went ( HANDLED_BY_SCRIPT , HANDLED_BY_AGENT , HANDLED_BY_AI , VOICEMAIL , MISSED , NO_ANSWER , BUSY , DECLINED …), endReason why it ended and endedBy who ended it. All values: Part 4 . Guards that protect every account (account limits , changeable with PATCH /v1/accounts/{id} ): Limit Default What happens maxCallMinutes 15 The call is ended ( MAX_CALL_LENGTH ) maxWaitSeconds 300 Longest wait in a waiting room, whatever the flow says callerCallsPer10Min 5 The same caller calling more often is refused ( REJECTED , CALLER_LIMIT ) otpPerDestinationPer10Min 3 More OTP calls to one number → refused voicemailMaxSeconds 120 Longest voicemail maxBlockVisits 25 Blocks run without any input from the caller before the call is ended ( LOOP_GUARD ) — stops endless loops 1.5 Calling people (call-outs) The engine places calls for an account and plays an Outgoing flow when the person answers. Way Endpoint Use Quick call POST /v1/calls "Your order is ready" to 1–1000 people, now. Created and started in one request Broadcast POST /v1/callouts kind: BROADCAST The same, but built in steps (create → add people → start), optionally at startsAt Campaign POST /v1/callouts kind: CAMPAIGN Calling a list over days: a daily window ( windowFrom / windowTo ), week days, pace per minute, up to 5 tries, a fallback App action (e.g. SMS) after the last failed try Scheduled callback made by a caller during a call (Schedule call block or AI schedule_call ) "Nipigie baada ya saa moja": a one-person call-out with ref schedule:: ; the client app sees it in GET /v1/callouts and the call.scheduled webhook OTP POST /v1/otp Read a code to a number Each person on a call-out is a recipient with their own vars (e.g. {"name": "Asha", "amount": "45000"} ) that the flow says as {name} , {amount} . Rules the engine applies to every call-out (the client app doesn'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 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, ".") { "id": "5b0c7f8e-…", "type": "call.ended", "createdAt": "2026-11-13T16:44:04Z", "data": { … } } 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 Recorded When Voicemail Always, when a flow reaches a Voicemail block (up to voicemailMaxSeconds ) Live parts Talk with an agent, when the account's recording.mode is LIVE_PARTS (default) and the department / block allows it. Callers hear the noticeText first Callers' spoken answers Short clips used for speech recognition; not kept as recordings After a recording: recording.ready webhook → GET /v1/calls/{id}/recording (MP3). If recording.transcribe is on (default), a transcript and a short summary follow: summary.ready → GET /v1/calls/{id}/transcript . Recordings are encrypted at rest and deleted after the account's retentionDays (default 90). 1.11 Costs Every call carries providerCost : what it cost Ringmaster in phone minutes 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 decides what to charge its customers; these are inputs, not prices. 1.12 Formats What Format Phone numbers E.164 with + : +255745051250 IDs UUID strings; your own ids go in externalRef (accounts), ref (scripts, agents) ref / externalRef Your text id, up to 120 characters; letters, digits, - , _ recommended Times in responses UTC ISO 8601 ( 2026-11-13T14:41:00.802627Z ) Times inside an account HH:mm in the account's timezone (opening hours, quiet hours, campaign windows) Languages sw (Kiswahili, default), en Lists GET /v1/calls pages with page (from 0) and size (default 50) → {calls, page, size, total} ; other lists return everything Money in costs TZS with 2 decimals, USD with up to 7 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): Part 5 . 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 ( Part 5 §5.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)" . Pushed back unchanged in a new version, "(hidden)" keeps the stored secret of the same block 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": ""} (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:///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 (