Textify Voice: Product & Functional Specification
Status: draft for implementation · Owner: Joshua Saimon Sakweli (QBIT SPARK) · Updated 2026-11-08
Companion:Companion docs:FLOWS.md(flow & action reference) ·ARCHITECTURE.md(components,flows,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 / call centerVoice as a new Textify module, so the same Textify customers can use it from the same portal and wallet. TheIt productcombines:
- a call center (one business number, menus, browser agents), similar in spirit to SemaCall (semacall.
com: "One business. One number. Every team")com),plusand - Textify's
ownstrength, bulk: voice broadcast campaigns with keypad actions and SMS fallback.
Voice isruns built ason a separate service, the Voice Engine (Java Spring Boot). It is an engine: itblind knows nothing aboutto users, logins, roles orand 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
ACustomersTextify customer getsget anumbernumber,withchooseahow it answers (Direct,greetingMenu,+laterkeypad menuAI), and calls reachhumanagents answering in the browser.Outbound:Callersagentswaitingmakefor an agent hearclick-hold music, never silence; nobody waits forever (voicemail).- Click-to-call
callsoutboundfromforthe browser.agents. - Broadcast campaigns:
send a recordedvoice message tothousandsthousands,ofkeypadnumbers,actionswith "press 1 to(talk tous"us, confirm via webhook, opt out…), retries,opt-out, andSMS fallback forpeoplenon-answerers. - Customers
didn'tcreateanswerwhat 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
by AI. - Missed calls / voicemails
→becomeautomaticfollow-ups in Textify (tickets, SMS/WhatsApp from Textify.WhatsApp). BillingPredictablepercosts: per-minute/callper answered callbilling from theexistingTextifywallet.wallet; voice-generation cost known before it's spent.- (Phase 3) AI receptionist
andwithAIaoutboundper-businesscallsknowledgeonbase,thehandingsameoverengine.to humans.
3. Non-goals
NoEngine has no users,auth,auth of people, roles,plansplans, prices, money, tickets orprices in the engine.contacts.- No approval workflows anywhere.
- No
self-service.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
v1v1.(seeNoARCHITECTURE §7).Python. - Countries other than Tanzania (later, same design).
4. Key terms (plain language)
| Term | Meaning |
|---|---|
| PSTN | Normal phone network (Vodacom, Airtel, Yas, Halotel, TTCL) |
| AT | Africa's Talking: our |
| Voice callback / XML | AT POSTs to our Play, GetDigits, Dial, Record…) |
| SIP / SIP trunk | |
| LiveKit room | |
| Browser agent | Staff answering |
| Department | A group of agents that calls ring ( |
| Flow | |
| One step: play, menu, collect, connect, voicemail, webhook, branch, sms, optout, hangup, ai | |
| Message | Something |
| Campaign | A broadcast: |
| 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…)
│ ├── Contacts, 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 |
| LiveKit (self-hosted, dedicated box) | |
| TTS: |
Light open-source model self-hosted on CPU (candidate: Piper Swahili voice), |
| TTS: Premium | OpenAI TTS or Azure Speech (Swahili neural voices), chosen by listening test |
| OpenAI | |
| MinIO (or Linode Object Storage) | Audio library + recordings |
6.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 |
|
| Toll-free incoming | TZS 200 / min |
| SIP trunk | TZS 8.61 / min |
How our calls combine:
| Call type | Approx. TZS / min |
|---|---|
| Inbound, |
17 |
| Inbound |
17 + 8.61 ≈ 25.61 |
| Click-to-call |
60 + 8.61 ≈ 68.61 |
| 60 per answered minute (unanswered likely free, confirm) | |
| 60 + 8.61 |
AgreedVoice facts: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).
Costcost follows minutes, notthe number of agents.agentsUnlimited(unlimitedagentsagents);pernoaccount.Nomobile forwardingto mobilesby default (17 + 60 =77/min).AI (Phase 3) adds OpenAI cost per minute. Measure in the PoC before pricing.
Agreed:
Indicative selling prices (finalize after PoC): number TZS 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; summary add-on TBD.250. Prices live in Textify only.Textify.
7. Personas
Business admin(Textify customer): number setup, greeting, menu, agents, ring groups, campaigns, call logs, recordings.Agent: logs into Textify, sets Available, answers in the browser, click-to-call.Caller / call recipient: anyone on any TZ network.Partner(later): resells voice under their own brand.
8. Functional requirements
8.1 Numbers & flowsanswer modes
- FR-N1: Ops registers AT numbers in the engine; Textify assigns a number to an account.
- FR-N2: Each number has
onean open-hours flow and a closed-hours flow (JSON,generatedbuilt in Textify's UI, stored and executed byfrom theengine)answer mode; FLOWS.md). - FR-N3:
FlowBusinessnodehourstypesper number (keeptimezonesmall)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:
say,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(GetDigits with options),,ringcollect(ring group)connect,voicemail,webhook,branch,sms,optout,hangup.,Phase(Ph3:3)ai. - FR-
N4:F3:FlowVariables:options:system,business hourscontact (open/closed branch)campaigns),recordingcollected,on/off,webhookring timeout, no-answer action.result. - FR-
N5:F4:SavingValidationaon 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
takesversion. - FR-F6:
onNever fail thenextcaller:call.anyNoerrorapproval.→ stored system message + graceful hangup.
8.23 InboundMessages & 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 →
engine looks upnumber → account →flow.flow (open/closed by hours). - FR-I2: Balance check
:enginewithasks Textify "can this account take a call?". TextifyTextify; unreachable → allow (fail-open). FR-I3:No balance →shortsystem message, hang up.- FR-
I4: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-
I5:I4:Greetingconnectandtomenuarundepartment → engine rings available agents in their browsers → caller waits on ATXMLwith(noholdLiveKitmusiccost).→ FR-I6:agentringnodeaccepts → callisdialed into LiveKit →caller hears hold audio → engine rings the group's available agents in their browsers.connected.- FR-
I7:I5: Waiting v1:if all agents are busy, the caller stays onholdand the engine rings again as agents become freemusic (engine-sidesystemqueue)default or customer upload),untiloptionalthecomforttimeout.message, FR-I8:maxTimeoutwait →noAnsweraction:(usuallyvoicemail,voicemail),hangup,optional keys while waiting (Phasee.g.3)1AI.= voicemail).- FR-
I9:I6:NotEachhandleddepartmentbyhasanyonea→waitingorderMISSED→(oldesteventfirst).→WhenTextifyansendsagent becomes available, theSMS/WhatsAppnextfollow-up.waiting caller rings them.
8.3 Browser agents
- FR-
H1:I7:Agents are only an opaqueAgents:agentRef+ display nameonly in theengine.engine;Textify owns the user. FR-H2: StatusstatusAVAILABLE / BUSY / OFFLINE. Online; presenceisautomatic while theagent'sbrowser is connected.- FR-
H3: Ring popup + ringtone in the browser;first to accept wins; others' popups close. FR-H4:I8: Ring strategy pergroup:department:ALL_AT_ONCE(default) orROUND_ROBIN; first to accept wins.- FR-
H5:I9:Mute,In-call:hangmute,up,hold (with music), transfer toanotheragent/department,agenthangor group.up. - FR-
H6:I10:OptionalRingtransferpopup shows the caller number; Textify adds the contact name and open tickets (screen pop, Textify-side). - FR-I11 (later, designed): position announcements, estimated wait, callback request (virtual hold), whisper to
anagent,externalwrap-upmobile (off by default; extra cost).time.
8.45 Outbound: click-to-call
- FR-O1: Agent clicks a number
in Textify→ engineplaces the callcalls via AT from the account's number(caller→IDagent= that number). FR-O2: Agent is alreadywaits in thecallroomin→the browser and hears ringing. Customercustomer answers → connected.- FR-
O3:O2: Balance check before dialing.
8.56 Outbound:Campaigns broadcast campaigns(broadcast)
- FR-C1:
TextifyCampaigncreates a campaign:= account, from-number,audioflow (uploadedmessagefile+URL)keypador TTS text,actions), recipientslist,(with variable columns), start time, calling window (default 08:00–20:00 EAT), calls/sec + maxcalls per second /concurrent,retry policyretries (default2 retries,2, 30 min apart, for no-answer/busy). - FR-C2:
OptionalPersonalizedkeypadmessagesoptionsgeneratedafterbeforethe message:1→ talk to agentsdialing (ring group),9→ opt out. (Phase 3:1→ AI.)FR-M4). - FR-C3: Opt-out
/ do-not-calllist peraccount,account, always checked before dialing. - FR-C4:
PauseStart / pause / resume /cancelcancel;any time (Textify pauses automaticallywhenonthelowwalletwallet;runsenginelow).pauses on generation failure. - FR-C5: Per-recipient
result:results:status,ANSWERED(seconds,keypress),keysNO_ANSWER,pressed,BUSY,actionsFAILED,triggered,OPTED_OUT.cost. - FR-C6: Progress
events (batched)+ completionevent.events; Textify canSMSSMS/WhatsApp everyone who didn'tanswer(key selling point).answer. - FR-C7: Throughput limited by
AT'sATcalls-per-secondCPS /concurrent-call limits for our accountconcurrency (confirm).; Textify shows ETA.
8.67 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, transcription,transcript, summary
- FR-R1: Recording per number/campaign (default on for agent calls, with notice)
.; FR-R2:recordedEnginebydownloadsAT,every recordingcopied into our storage immediately.- FR-
R3R2 (PhasePh 2): transcript(Swahili/English)+ summaryJSON:JSON2–3 line(summary, intent, resolved, follow-ups, sentiment,topic.topic). - FR-
R4:R3: Retention default 90 days (per-accountpersetting)account). Tanzania Personal Data Protection Act applies.
8.79 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:
OnPercall end the engine emits usage:call: direction,seconds per leg (pstnSeconds,trunkSeconds,aiSeconds),AT's reportedATamount, recorded,summary.actions triggered. - FR-B2: Per generation:
ttsCharacters, tier. Per campaign: totals. - FR-B3: Textify prices and
deducts. Thededucts; engine never handles money. FR-B3:Eventsare idempotent (event id),idempotent, signed,retried until 2xx.retried.
8.8 Dashboard (in Textify; data from the engine)
Call log, call detail (audio, transcript, summary), number + flow editor, agents and status, ring groups, campaigns (progress, results, "SMS non-answerers"), simple stats (answered/missed, by agent, campaign answer rate).
9. Non-functional
| Area | Requirement |
|---|---|
| Availability | Calls |
| Isolation | Dedicated Voice |
| Latency | AT callback response < 500 ms; agent ring < 3 s; connect after accept within one wait segment (~10–15 s |
| Security | SIP |
| Privacy | Recording notice, retention, |
| Predictable cost | No runtime TTS; premium cost known in advance |
| Simplicity | REST + webhooks + Postgres + Redis + |
10. Phases
Phase 0: Proof of concept (on the working AT number)
Working AT number (done) → set callback URL to the engineCallback →SayPlaygreetinga file works.GetDigitsmenu works; keys arrive.- Wait loop:
GetDigitswrappingPlayhold segment, callback every segment. Dial sip:{callId}@<livekit-sip-host>from the XML→caller lands in LiveKitroomcall-{callId}.Browser→ browser test pagejoins the room → two-way audio withhears the caller.CheckFinalATcallbackfinalfieldscallback:(duration, amount,recording URL.recordingUrl).Outbound: ATOutbound call API → answer → Dial intoLiveKitLiveKit.- Voice
browserlisteninghearstest:thesamecustomer.Swahili sentences in Basic candidate (Piper), Premium candidates (OpenAI, Azure), and ATSayfor reference, played on real phones to 5–10 Tanzanians.
Phase 1: Call center MVP
Accounts,Numbers; numbers,answer flowsmodes Direct + Menu; flow engine with nodes play, menu, collect, connect, voicemail, webhook (; messages (audio upload/record + text Basic tier); waiting v1; browser say/play/menu/ring/voicemail/hangup)async), branch, sms, optout, hangupagentsagents, (lobby,departments, ring, accept), ring groups + waiting,hold/transfer; click-to-call,call; recording to our storage,storage; missed-callmissed/voicemail events,events; usage events,events; balance check.check; test call; endpoints.
Phase 2: Bulk + intelligence
Broadcast campaignsCampaigns (throttle,all window,FR-C); retries,Premium opt-out,voice; press-1-to-agent,sync results,webhooks; SMS fallback via Textify), OpenAI transcriptiontranscripts + summaries,summaries; businessstats hours,per stats.node/campaign.
Phase 3: AI
ai flownode node: OpenAI Realtime joins the LiveKit room over SIP, controlled from Java;/ AI →answer humanmode; knowledge base; AI tools via endpoints; handover; improvement loop; AI outbound campaigns (reminders, confirmations, surveys). Swahili quality test with real callers first.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 engine 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'sAT SIP signalling + media IPs (for our firewall andfirewall, LiveKit allowlist).- Billing for Dial-to-SIP:
is it17 (or 60 outbound) + 8.61trunk? Anything else? - Does
Dial record="true"recordaDial-to-SIPDiallegs?toFormat,SIP,howandlongwhat'sAT keeps therecording format and retention on AT's side?file? Calls-per-secondCPS and max concurrent calls (inboundin +outbound)out) for our account; can they be raised?- Outbound
billingbilled per second or per started minute?Are unanswered/Unanswered/busycallsfree? Can the outboundOutbound callAPI takeAPI: multiple numbers per request?WhatReturned 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 itreturnstream while downloading; any caching/pre-upload of media?- After a
Dialends unanswered, does AT continue to the next XML (sessioncallbackids)after Dial)? DoAirtelvoice callbacks come from fixed IPs? Is there anycallback signature?The regular number isAirtel:number: any issues for callers from Vodacom, Yas, Halotel, TTCL?- TCRA:
doeslicence for resellingvoicevoice;need a licence on our side? Rulesrules for automated/marketing calls (consent, hours, caller ID)?. In writing.
12. Risks
| Risk | Mitigation |
|---|---|
| AT can't Dial to our SIP URI | |
| Wait loop limits (callbacks/timeouts) | PoC step 3; fallback: wait inside LiveKit with ingress music (no |
| Basic TTS not good enough / licence not commercial | Listening test; Basic = Premium at lower price, or audio upload only |
| AT throughput |
Confirm Q5; |
| Swahili AI quality ( |
Real-caller test; |
| SIP attacks / webhook SSRF | |
| Textify outage | Fail- |
| Regulation (TCRA, PDPA) | Written answers; notices; opt-out; retention |
13. Decisions log
| # | Decision |
|---|---|
| D1 | |
| D2 | AT is the only PSTN path, both directions |
| D3 | Dial → LiveKit |
| D4 | |
| D5 | |
| D6 | No Python; Ph 3 AI = OpenAI Realtime over SIP controlled from Java (Python worker only as fallback) |
| D7 | |
| Service auth: API |
|
| Communication: REST + webhooks + Postgres outbox/ |
|
| D9 | Engine owns media (MinIO, ffmpeg, signed play URLs); Textify references mediaId |
| D10 | <Say>, |
| 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 |
| No approval workflows |