Ringmaster Voice: Product & Functional Specification
Status: draft for implementation · Owner: Joshua Saimon Sakweli (QBIT SPARK) · Updated 2026-11-08 Companion docs:
FLOWS.md(flow & action reference) ·ARCHITECTURE.md(components, data model, APIs, infra). Written so a new engineer, or a new AI coding session, can pick up the work with no prior context.
1. Background
Textify Africa (textify.africa) is an existing bulk SMS + WhatsApp platform, written in Go, live in Tanzania, Uganda and Nigeria. Its main model is white-label: partners brand it as their own and onboard their own customers, who top up a prepaid wallet.
We're adding Voice as a new Textify module, so the same Textify customers use it from the same portal and wallet. It combines:
- a call center (one business number, menus, browser agents), similar in spirit to SemaCall (semacall.com), and
- Textify's strength, bulk: voice broadcast campaigns with keypad actions and SMS fallback.
Voice runs on a separate service, the Voice Engine (Java Spring Boot). It is an engine: blind to users, logins, roles and money. It is told what to do with numbers and calls and reports what happened. Textify consumes it like any other API. Other QBIT SPARK products (JikoXpress, Mauzodukani, NexGate) can consume it later.
Status: we already have a working Tanzanian number from Africa's Talking.
2. Goals
- Customers get a number, choose how it answers (Direct, Menu, later AI), and calls reach agents answering in the browser.
- Callers waiting for an agent hear hold music, never silence; nobody waits forever (voicemail).
- Click-to-call outbound for agents.
- Broadcast campaigns: voice message to thousands, keypad actions (talk to us, confirm via webhook, opt out…), retries, SMS fallback for non-answerers.
- Customers create what callers hear by uploading/recording audio or typing text (Basic or Premium voice).
- Keypresses can trigger the customer's own systems (webhooks with parameters).
- Every call is logged, optionally recorded, and (Phase 2) transcribed + summarized.
- Missed calls / voicemails become follow-ups in Textify (tickets, SMS/WhatsApp).
- Predictable costs: per-minute call billing from the Textify wallet; voice-generation cost known before it's spent.
- (Phase 3) AI receptionist with a per-business knowledge base, handing over to humans.
3. Non-goals
- Engine has no users, auth of people, roles, plans, prices, money, tickets or contacts.
- No approval workflows anywhere.
- No live text-to-speech during calls (no AT
<Say>; all text becomes audio files first). - No video over phone lines. No message broker (Kafka/RabbitMQ/MQTT) in v1. No Python.
- Countries other than Tanzania (later, same design).
4. Key terms
| Term | Meaning |
|---|---|
| PSTN | Normal phone network (Vodacom, Airtel, Yas, Halotel, TTCL) |
| AT | Africa's Talking: our numbers, PSTN both directions, voice callbacks/XML |
| Voice callback / XML | AT POSTs to our URL; we reply with XML actions (Play, GetDigits, Dial, Record…) |
| SIP / SIP trunk | Internet call protocol / the AT → LiveKit connection carrying calls |
| LiveKit room | Live audio session; each connected call = one room (caller + agent, later AI) |
| Browser agent | Staff answering in the Textify web app |
| Department | A group of agents that calls ring (a ring group) |
| Flow | Graph of nodes deciding what happens on a call (FLOWS.md) |
| Node / action | One step: play, menu, collect, connect, voicemail, webhook, branch, sms, optout, hangup, ai |
| Message | Something callers hear: uploaded audio, or text turned into audio (Basic/Premium voice) |
| Campaign | A broadcast: a flow run for every answered call on a recipient list |
| Endpoint | A customer's saved webhook URL + secret, used by webhook actions |
| Knowledge base (KB) | Business facts the AI uses (Phase 3) |
| DTMF | Keypad presses |
5. Product structure (what customers see in Textify)
Voice
├── Numbers get/assign numbers
├── Call Center inbound + agents
│ ├── Answer mode: Direct | Menu | AI (Ph 3)
│ ├── Departments & agents
│ ├── Business hours, closed message
│ ├── Waiting: hold music, comfort message, max wait → voicemail
│ ├── Recording on/off
│ └── Click-to-call (agents calling one customer)
├── Campaigns bulk outbound
│ ├── Message (audio or text, variables)
│ ├── Keypad actions (talk to dept, webhook, sms, opt-out…)
│ ├── Recipients picked from Textify's EXISTING contacts / groups (same as SMS)
│ ├── Schedule, calling hours, retries
│ └── Results + SMS fallback to non-answerers
├── Audio library uploads, recordings, generated voices
├── Integrations webhook endpoints
├── Knowledge base (Ph 3, for AI)
└── Calls & Tickets call log, recordings, summaries; follow-up tickets (Textify-only feature)
Answer-mode templates generate flows; customers never see raw nodes (an advanced flow editor may come later; the format already supports it).
| Answer mode | Caller experience | AT actions |
|---|---|---|
| Direct | Greeting → ring department → wait with music → voicemail | Play → (wait loop) → Dial → Record |
| Menu | Greeting → "press 1 sales, 2 support" → that department → voicemail | Play + GetDigits → wait loop → Dial → Record |
| AI (Ph 3) | AI answers from the knowledge base, hands over to a department | Dial → LiveKit + OpenAI → agents |
6. Providers
| Provider | Role |
|---|---|
| Africa's Talking | Numbers, PSTN in/out, callbacks + XML, outbound call API, call recording, SIP to LiveKit |
| LiveKit (self-hosted, dedicated box) | Rooms, browser agents (WebRTC), agent-hold audio, Ph 3 AI participant. Separate from NexGate's LiveKit |
| TTS: Basic | Light open-source model self-hosted on CPU (candidate: Piper Swahili voice), only if quality and commercial licence pass the listening test |
| TTS: Premium | OpenAI TTS or Azure Speech (Swahili neural voices), chosen by listening test |
| OpenAI | Ph 2 transcription + summaries; Ph 3 live AI (Realtime over SIP, controlled from Java) |
| MinIO (or Linode Object Storage) | Audio library + recordings |
7. Costs (AT Tanzania, africastalking.com/pricing, Oct 2026)
| Item | Price |
|---|---|
| Regular number (Airtel) | Setup TZS 100,000 + 18% VAT; TZS 35,000/month + 18% VAT |
| Toll-free | Same + TZS 170,000 deposit |
| Incoming / Outgoing | TZS 17 / TZS 60 per min |
| Toll-free incoming | TZS 200 / min |
| SIP trunk | TZS 8.61 / min |
| Call type | Approx. TZS / min |
|---|---|
| Inbound, menu / waiting / voicemail (stays on AT) | 17 |
| Inbound talking to browser agent | 17 + 8.61 ≈ 25.61 |
| Click-to-call | 60 + 8.61 ≈ 68.61 |
| Broadcast message only | 60 per answered minute (unanswered likely free, confirm) |
| Broadcast → talk to agent | 60 + 8.61 while talking |
Voice generation (TTS): Basic = fixed server cost; Premium ≈ $15–16 per 1M characters (check current), always calculated before spending (at save or campaign creation). No AT <Say> usage. Waiting happens on AT (no trunk cost while waiting).
Agreed: cost follows minutes, not agents (unlimited agents); no mobile forwarding by default (77/min). Indicative selling prices (finalize after PoC): number 60–75k/month; inbound answered 40–50/min; outbound 90–100/min; broadcast 100–150 per answered call; premium voice per message/character; AI minute 150–250. Prices live in Textify.
8. Functional requirements
8.1 Numbers & answer modes
- FR-N1: Ops registers AT numbers in the engine; Textify assigns a number to an account.
- FR-N2: Each number has an open-hours flow and a closed-hours flow (generated from the answer mode; FLOWS.md).
- FR-N3: Business hours per number (timezone Africa/Dar_es_Salaam), with holidays list (optional).
- FR-N4: Changes take effect on the next call; every save is a new flow version.
- FR-N5: Test call: engine calls the customer's own phone and runs the flow.
8.2 Flows & actions (full reference: FLOWS.md)
- FR-F1: One action system for inbound, campaigns and (Ph 3) AI.
- FR-F2: Node types:
play,menu,collect,connect,voicemail,webhook,branch,sms,optout,hangup, (Ph 3)ai. - FR-F3: Variables: system, contact (campaigns), collected, webhook result.
- FR-F4: Validation on save (no dangling outcomes, size limits, loop guard, variables exist, media ready, targets belong to account). No approvals.
- FR-F5: Running campaigns are pinned to their flow version.
- FR-F6: Never fail the caller: any error → stored system message + graceful hangup.
8.3 Messages & voice (what callers hear)
- FR-M1: Every message is either uploaded/recorded audio or text.
- FR-M2: Audio: mp3/wav/m4a/ogg, ≤ 10 MB, ≤ 5 min, or record in the browser → engine converts to phone-friendly MP3.
- FR-M3: Text: language (sw/en), voice, tier Basic or Premium; max 500 characters per message.
- FR-M4: Text becomes an audio file when saved (flows) or before dialing (campaigns, incl. per-recipient personalized text). No TTS during live calls.
- FR-M5: Generated audio is cached by text + language + voice + tier; identical text is generated once.
- FR-M6: Swahili number/currency/date normalization before TTS ("TZS 25,000" → "shilingi elfu ishirini na tano").
- FR-M7: Premium cost shown before saving/starting; reported in usage (
ttsCharacters, tier). - FR-M8: Generation failure: flow save shows an error and the previous version stays live; campaign stays unstarted or pauses (
TTS_FAILED). Safety net at call time: a stored system file. - FR-M9: Listen/preview in the browser for audio and generated text.
8.4 Inbound, waiting and agents
- FR-I1: AT callback → number → account → flow (open/closed by hours).
- FR-I2: Balance check with Textify; unreachable → allow (fail-open). No balance → system message, hang up.
- FR-I3: Recording notice when recording is on ("Simu hii inaweza kurekodiwa kwa ajili ya ubora wa huduma"; in English: "This call may be recorded for quality of service").
- FR-I4:
connectto a department → engine rings available agents in their browsers → caller waits on AT with hold music → agent accepts → call dialed into LiveKit → connected. - FR-I5: Waiting v1: hold music (system default or customer upload), optional comfort message, max wait →
noAnswer(usually voicemail), optional keys while waiting (e.g. 1 = voicemail). - FR-I6: Each department has a waiting order (oldest first). When an agent becomes available, the next waiting caller rings them.
- FR-I7: Agents:
agentRefonly in the engine; statusAVAILABLE / BUSY / OFFLINE; presence automatic while the browser is connected. - FR-I8: Ring strategy per department:
ALL_AT_ONCE(default) orROUND_ROBIN; first to accept wins. - FR-I9: In-call: mute, hold (with music), transfer to agent/department, hang up.
- FR-I10: Ring popup shows the caller number; Textify adds the contact name and open tickets (screen pop, Textify-side).
- FR-I12: Agents need no browser extension or install: Textify web page + microphone permission; one click "Go available" unlocks the ringtone; background-tab alerts via browser notifications; optional installable PWA.
- FR-I11 (later, designed): position announcements, estimated wait, callback request (virtual hold), whisper to agent, wrap-up time.
8.5 Outbound: click-to-call
- FR-O1: Agent clicks a number → engine calls via AT from the account's number → agent waits in the room → customer answers → connected.
- FR-O2: Balance check before dialing.
8.6 Campaigns (broadcast)
- FR-C1: Campaign = account, from-number, flow (message + keypad actions), recipients (with variable columns), start time, calling window (default 08:00–20:00 EAT), calls/sec + max concurrent, retries (default 2, 30 min apart, for no-answer/busy).
- FR-C1b: Recipients come from Textify's existing contacts and groups (the same ones used for SMS/WhatsApp); contact fields become campaign variables. Textify sends the engine only
{e164, vars}per recipient. No separate call-center contact list. - FR-C2: Personalized messages generated before dialing (FR-M4).
- FR-C3: Opt-out list per account, always checked before dialing.
- FR-C4: Start / pause / resume / cancel; Textify pauses automatically on low wallet; engine pauses on generation failure.
- FR-C5: Per-recipient results: status, seconds, keys pressed, actions triggered, cost.
- FR-C6: Progress + completion events; Textify can SMS/WhatsApp everyone who didn't answer.
- FR-C7: Throughput limited by AT CPS / concurrency (confirm); Textify shows ETA.
8.7 Webhook actions (customer integrations)
- FR-W1: Customers save endpoints (HTTPS URL + secret) in Textify; stored in the engine per account.
- FR-W2:
webhooknode sends parameters with variables ({caller}, contact columns,{key},{input.x},{callId},{campaignId}…). - FR-W3: Async (default, Phase 1): queued, retried up to 24 h; caller hears a fixed confirmation immediately.
- FR-W4: Sync (Phase 2): engine waits ≤ 3 s; the response can set variables (
{result.x}) and a message to play (generated on demand, then cached). - FR-W5: Signed (HMAC), idempotency key per call + node, HTTPS only, private/internal IPs blocked, failures never break the call.
8.8 Recording, transcript, summary
- FR-R1: Recording per number/campaign (default on for agent calls, with notice); recorded by AT, copied into our storage immediately.
- FR-R2 (Ph 2): transcript + summary JSON (summary, intent, resolved, follow-ups, sentiment, topic).
- FR-R3: Retention default 90 days (per account). Tanzania Personal Data Protection Act applies.
8.9 AI receptionist + knowledge base (Phase 3)
- FR-A1: Answer mode AI or
ainode anywhere (e.g. menu option, campaign key, noAnswer). - FR-A2: Swahili first, English if the caller speaks it; short spoken answers; never invents prices/policies.
- FR-A3: Knowledge base per account, edited in Textify, stored in the engine: business profile (name, about, location, hours), services & prices, FAQs, departments with descriptions (for routing), rules (never do / when to hand over), optional documents (PDF/text).
- FR-A4: v1 of AI: KB placed directly into the AI's instructions (no vector DB). Large KBs:
search_knowledgetool over Postgres full-text/pgvector. - FR-A5: Live data via the customer's endpoints as AI tools (same endpoints as
webhook). - FR-A6: Handover to a department (warm transfer); no agent → take a message.
- FR-A7: Improvement loop: summaries flag questions the AI couldn't answer; Textify suggests adding them to the FAQ.
8.10 Follow-ups & tickets (Textify-only)
- Built in Textify from engine events: missed call → "call back" ticket; voicemail → ticket with recording; unresolved AI call / follow-ups from summaries → ticket; manual "create ticket" by agents.
- Fields: phone (+ contact), subject, status
OPEN/PENDING/CLOSED, assignee/department, notes, linked calls/recordings/SMS/WhatsApp, optional due date. Customer timeline per phone number. No SLAs/escalations in v1. - The engine has no ticket concept.
8.11 Usage & billing
- FR-B1: Per call: direction,
pstnSeconds,trunkSeconds,aiSeconds, ATamount, recorded, actions triggered. - FR-B2: Per generation:
ttsCharacters, tier. Per campaign: totals. - FR-B3: Textify prices and deducts; engine never handles money. Events idempotent, signed, retried.
9. Non-functional
| Area | Requirement |
|---|---|
| Availability | Calls continue if Textify is down (fail-open balance check, queued events) |
| Isolation | Dedicated Voice box (not shared with NexGate) |
| Latency | AT callback response < 500 ms; agent ring < 3 s; connect after accept within one wait segment (~10–15 s max) |
| Security | SIP only from AT; API keys + HMAC; verified webhooks; SSRF protection (ARCHITECTURE §8) |
| Privacy | Recording notice, retention, own storage, signed URLs |
| Predictable cost | No runtime TTS; premium cost known in advance |
| Simplicity | REST + webhooks + Postgres + Redis + LiveKit + MinIO; no broker, no OAuth, no approvals |
10. Phases
Phase 0: Proof of concept (on the working AT number)
- Callback →
Playa file works. GetDigitsmenu works; keys arrive.- Wait loop:
GetDigitswrappingPlayhold segment, callback every segment. Dial sip:{callId}@<livekit-sip-host>→ roomcall-{callId}→ browser test page hears the caller.- Final callback fields (duration, amount, recordingUrl).
- Outbound call API → answer → Dial into LiveKit.
- Voice listening test: same Swahili sentences in Basic candidate (Piper), Premium candidates (OpenAI, Azure), and AT
Sayfor reference, played on real phones to 5–10 Tanzanians.
Phase 1: Call center MVP
Numbers; answer modes Direct + Menu; flow engine with nodes play, menu, collect, connect, voicemail, webhook (async), branch, sms, optout, hangup; messages (audio upload/record + text Basic tier); waiting v1; browser agents, departments, hold/transfer; click-to-call; recording to our storage; missed/voicemail events; usage events; balance check; test call; endpoints.
Phase 2: Bulk + intelligence Campaigns (all FR-C); Premium voice; sync webhooks; transcripts + summaries; stats per node/campaign.
Phase 3: AI
ai node / AI answer mode; knowledge base; AI tools via endpoints; handover; improvement loop; AI outbound campaigns.
Later (already designed): position announcements, estimated wait, callback request, whisper, wrap-up time, advanced flow editor, partner white-label voice, other countries, other QBIT products as clients.
11. Questions for Africa's Talking (before Phase 1)
- Can XML
Dialtarget an external SIP URI with a custom user part (sip:{callId}@our-host)? Transport, port, codecs (G.711), DTMF method? - AT SIP signalling + media IPs (firewall, LiveKit allowlist).
- Billing for Dial-to-SIP: 17 (or 60 outbound) + 8.61? Anything else?
- Does
Dial record="true"record Dial-to-SIP legs? Format, how long AT keeps the file? - CPS and max concurrent calls (in + out) for our account; can they be raised?
- Outbound billed per second or per started minute? Unanswered/busy free?
- Outbound call API: multiple numbers per request? Returned session ids?
- Callback source IPs; any signature on callbacks?
GetDigitswrappingPlay: keys interrupt playback? Max timeout? Any limit on how many callbacks per call (our wait loop)?Play: timeout for fetching the URL; does it stream while downloading; any caching/pre-upload of media?- After a
Dialends unanswered, does AT continue to the next XML (callback after Dial)? - Airtel number: any issues for callers from Vodacom, Yas, Halotel, TTCL?
- TCRA: licence for reselling voice; rules for automated/marketing calls (consent, hours, caller ID). In writing.
12. Risks
| Risk | Mitigation |
|---|---|
| AT can't Dial to our SIP URI | PoC step 4 first; fallback AT SIP softphones for agents |
| Wait loop limits (callbacks/timeouts) | PoC step 3; fallback: wait inside LiveKit with ingress music (no keys while waiting) |
| Basic TTS not good enough / licence not commercial | Listening test; Basic = Premium at lower price, or audio upload only |
| AT throughput slows broadcasts | Confirm Q5; throttle; ETA |
| Swahili AI quality (Ph 3) | Real-caller test; human handover always available |
| SIP attacks / webhook SSRF | AT-only firewall; endpoint IP blocking |
| Textify outage | Fail-open, outbox retries |
| Regulation (TCRA, PDPA) | Written answers; notices; opt-out; retention |
13. Decisions log
| # | Decision |
|---|---|
| D1 | Separate Voice Engine (Java 21, Spring Boot), blind to users/auth/money; Textify (Go) consumes it via REST + webhooks |
| D2 | AT is the only PSTN path, both directions |
| D3 | Greeting/menu/collect/voicemail/waiting run on AT XML; anything live goes Dial → LiveKit |
| D4 | All agents answer in the browser via LiveKit |
| D5 | Adapters: Telephony (AT), Media (LiveKit), TTS (Basic/Premium), AI (OpenAI): plain Java interfaces |
| D6 | No Python; Ph 3 AI = OpenAI Realtime over SIP controlled from Java (Python worker only as fallback) |
| D7 | Service auth: API keys in, HMAC webhooks out, secret URL + IP allowlist for AT, built-in signatures for LiveKit/OpenAI |
| D8 | Communication: REST + webhooks + Postgres outbox/jobs + Redis + LiveKit data messages; no broker in v1 |
| D9 | Engine owns media (MinIO, ffmpeg, signed play URLs); Textify references mediaId |
| D10 | Messages = audio or text; text → audio file at save / before dialing; no AT <Say>, no runtime TTS |
| D11 | Voice tiers Basic (self-hosted light model) / Premium (OpenAI or Azure), decided by listening test |
| D12 | One flow/action system (11 node types) for inbound, campaigns and AI (FLOWS.md) |
| D13 | Customers pick answer-mode templates (Direct / Menu / AI), not a flowchart editor (later maybe) |
| D14 | Waiting happens on AT (hold loop), not in LiveKit; LiveKit audio only for agent hold |
| D15 | Webhook actions via saved endpoints; async in Ph 1, sync in Ph 2; called by the engine directly |
| D16 | Knowledge base lives in the engine per account; prompt-stuffing first, search tool when large |
| D17 | Tickets/contacts/screen-pop live in Textify only |
| D18 | Dedicated Voice box, separate from NexGate's LiveKit |
| D19 | No approval workflows |
| D20 | Consumers are client apps with a managed lifecycle: registered only by the engine owner via the voice-admin CLI over SSH (no public ops API) → numbers allocated → API key + webhook secret shown once, stored hashed, handed over directly into the service's env → consume → rotate → suspend (inbound keeps working by default) → remove (export, release numbers, delete after grace) (ARCHITECTURE §8.1) |
| D22 | Contacts are Textify's existing contact module, reused as-is (same lists/groups as SMS/WhatsApp). Call center adds no contact store. The engine only receives phone numbers + variable values per campaign and caller numbers on calls; Textify does name lookup and screen pop |
| D21 | Browser agents need no extension or app install: a normal web page with WebRTC; optional PWA later |
No comments to display
No comments to display