Textify Voice: Product & Functional Specification
Status: draft for implementation · Owner: Joshua Saimon Sakweli (QBIT SPARK)
Companion·doc:Updated 2026-11-08 Companion:ARCHITECTURE.md(components, flows, data model, APIs, infra).This file is writtenWritten 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: agencies and resellers ("partners")partners brand the platformit as their own, set their own prices, and onboard their own customers.customers, Customerswho top up a prepaid wallet with mobile money or card..
We areWe're adding avoice third/ channel:call center as a new Textify module, so the callsame centerTextify / business phone linecustomers product,can use it from the same portal and wallet. The product is similar in spirit to SemaCall (semacall.com):com: "One business. One number. Every team."team"), Customersplus call one number; the call reaches the right person (or an AI answers) in the customer'Textify's language.own strength, bulk: voice broadcast with SMS fallback.
The voice capabilityVoice is built as a separate serviceservice, the Voice Engine ("Voice Service") in Java Spring BootBoot). It is an engine: it knows nothing about users, logins, roles or money. It is told what to do with numbers and calls and reports what happened. Textify consumes it aslike any other API. Other QBIT SPARK products (JikoXpress, Mauzodukani, NexGate) can consume it later.
Status: we already have a client.working TheTanzanian Voicenumber Servicefrom must not know about Textify'Africa's users, auth or money.Talking.
2. Goals
- A Textify customer
can getgets aTanzaniannumberphonewithnumbera greeting + keypad menu, andhavecallsevery incoming call answered, by anAI receptionist(Swahili first, English supported) or byreach human agents answering in the browser. AIOutbound:canagents makehandclick-to-call calls from thecallerbrowser.- Broadcast: send a recorded voice message to
athousandshumanof numbers, with "press 1 to talk to us", retries, opt-out, and SMS fallback for people who didn't answerwhen needed.. - Every call is logged, optionally
recorded, transcribedrecorded, and (Phase 2) transcribed + summarized by AI. - Missed calls
trigger an→ automatic SMS/WhatsAppfollow-upfrom(Textify already does messaging).Textify. Usage isbilledBilling per minute / per answered call from the existing Textifywallet.wallet.The(PhaseVoice Service is3)reusableAI receptionistbyandotherAIQBIToutboundSPARKcallsproductsonlaterthe(JikoXpress,sameMauzodukani, NexGate) without changes.engine.
3. Non-goals (for now)
- No
videousers,calls over phone lines (LiveKit SIP does not support video over SIP anyway). No user accounts,auth, roles, plans orpricingpricesinsidein theVoice Service.engine.- No approval workflows anywhere.
Keep everythingEverything self-serviceservice. - No
simple.video over phone lines. - No message broker (Kafka/RabbitMQ/MQTT) in v1 (see ARCHITECTURE §7).
- Countries other than Tanzania (
Uganda/Nigerialater,using thesame design). Building our own telco connection. We use Africa's Talking for numbers and the PSTN.
4. Key terms (plain language)
| Term | Meaning |
|---|---|
| PSTN |
|
| AT | Africa's Talking: our numbers and connection to all TZ networks. |
| Voice callback | AT POSTs to our URL when something happens on a call; we reply with XML instructions (Say, Play, GetDigits, Dial, Record…). |
| SIP | |
| SIP trunk | |
| LiveKit room | A live audio session. Each |
| Staff answering calls in the Textify web app (LiveKit |
|
| Flow | What happens when a number rings: greeting → menu → route. Stored as JSON. |
| Endpoint | Something that |
| Campaign | A broadcast: one audio message to |
| DTMF | Keypad presses |
5. Providers we use and why
| Provider | Role | |
|---|---|---|
| Africa's Talking |
||
| LiveKit (self-hosted, |
||
| OpenAI |
6. Costs (Africa's TalkingAT Tanzania, as published on africastalking.com/pricing, Oct 2026)
| Item | Price |
|---|---|
| Regular number (Airtel) | Setup TZS 100,000 + 18% VAT; TZS 35,000/month + 18% VAT |
| Toll-free number | Same |
| TZS 17 / min | |
| TZS 60 / min | |
| Toll-free incoming | TZS 200 / min |
| SIP | |
| TZS 8.61 / min | |
| TZS | |
6.1
How the costs combine
Every call has leg 1 (customer ⇄ our ATcalls number) and, if someone answers, leg 2 (our number ⇄ whoever answers).combine:
Customer ──leg 1──> AT number ──leg 2──> answerer
17 in / 60 out SIP trunk to LiveKit: 8.61
forward to a mobile: 60 (avoid)
| Approx. |
|
|---|---|
| Inbound, only greeting/menu/voicemail (no agent) | 17 |
| Inbound |
17 + 8.61 ≈ |
| 60 + 8.61 ≈ |
|
| Broadcast, caller presses 1 → agent | 60 + |
KeyAgreed facts already agreed:facts:
- Cost follows
callminutes, not the number of agents.IdleUnlimited agentscostpernothing, so customers can add unlimited agents.account. AvoidNo forwarding tomobiles.mobiles by defaultAgents(17answer+in60the=browser/app.77/min).Design decision:money is not the constraint, soall calls go through the SIP trunk into LiveKitAI (onePhasecall path, simpler code). We don't split IVR onto AT XML to save the 8.61.
6.2 AI cost (on top of telco)
An AI-answered minute3) adds OpenAI cost (Realtime, or STT + LLM + TTS). This is several times the telco cost per minute. ExactMeasure per-minute figures must be measured duringin the PoC frombefore realpricing.
calls
(token/audio usage logged per call).
6.3
Indicative selling prices (suggestion, to be finalizedfinalize after PoC):
| TZS 60– | |
Pricing livesTBD. Prices live in Textify onlyonly.. The Voice Service only reports usage.
7. Users & personasPersonas
- Business
owner /admin (Textify customer):getsnumberasetup,number,greeting,sets the AI greeting and business info, addsmenu, agents,seesringcalls,groups,recordingscampaigns,andcallsummaries.logs, recordings. Human agentAgent: logs intothe Textify web app,Textify, setsthemselves available,Available, answerscallsin the browser,makesclick-to-call outbound calls.call.- Caller / call recipient:
aanyonemember of the public calling the business number fromon any TZ network. - Partner (
reseller)later):later phase. Resellsresells voice under their ownbrand (same white-label model as SMS).brand.
8. Functional requirements
8.1 Numbers & flows
- FR-N1:
TextifyOpsadmin (QBIT/Textify ops) obtains numbers fromregisters ATand registers themnumbers in theVoiceengine;Service.Textify assigns a number to an account. - FR-N2:
AEach numberis assigned to exactlyhas oneaccountflow (JSON, built in Textify'stenantUI,id,storedopaqueandtoexecuted by theVoice Service)engine). - FR-N3:
EachFlownumbernodehastypesan(keepanswer modesmall):,AI_FIRSTsayplay,menu(AIGetDigitsanswers,withcan hand over)options),(ringHUMAN_ONLYringagents)group),,AI_ONLYvoicemailhangup. Phase 3:ai. - FR-N4:
ReassigningFloworoptions:releasingbusiness hours (open/closed branch), recording on/off, ring timeout, no-answer action. - FR-N5: Saving a
numberflowistakesaeffectsingleonAPIthe next call. No approval.
8.2 Inbound calls
- FR-I1:
CallerATdialscallbackthe→ engine looks up number →call arrives over the SIP trunkaccount →LiveKit creates a room → Voice Service records a callRINGING.flow. - FR-I2:
BeforeBalanceanswering,check:the Voice Serviceengine asks Textify "can this account take a call?"(prepaid balance).IfTextifyisunreachableunreachable,→ allowthe call(fail-open)and bill later.. - FR-I3:
IfNothebalanceaccount can't pay: play a→ shortpolitemessage,messagehangand end the call (no AI cost).up. - FR-I4:
Play arecording consentRecording 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:
AI_FIRST/AI_ONLY→ AI receptionist joinsGreeting andgreetsmenuwithruntheonaccount'sATgreeting.XML (no LiveKit cost). - FR-I6:
node →HUMAN_ONLYringringcall is dialed into LiveKit → caller hears hold audio → engine rings theaccount'group's available agents(ringingroup).theirIf nobody answers within N seconds (default 30) → voicemailorAI fallback (configurable) → markMISSEDif nobody handled it.browsers. - FR-I7:
CallerWaiting:keypressesif(DTMF)all agents aresupportedbusy,forthesimplecallermenusstays on hold and the engine rings again as agents become free ("pressengine-side1queue),foruntilsales").the timeout. - FR-I8: Timeout →
noAnsweraction: voicemail, hangup, (Phase 3) AI. - FR-I9: Not handled by anyone →
MISSED→ event → Textify sends the SMS/WhatsApp follow-up.
8.3 AI receptionist
FR-A1: SpeaksSwahili first, switches to English if the caller does. Handles code-switching (Swahili and English mixed).FR-A2: Uses the account'sinstructions(business info, FAQs, hours, tone) supplied by Textify.FR-A3: Short, spoken-style answers. Never invents prices or policies. If unsure → offer a human.FR-A4: Tools the AI can use (v1):transfer_to_human,take_message,end_call. Later: account-defined tools (check order, book appointment) via webhooks to the client app.FR-A5: Handover: AI callstransfer_to_human→ Voice Service rings availableBrowser agents→ first to accept joins the room → AI briefly introduces the context (warm transfer), then leaves.FR-A6: If no agent is available: AI takes a message (name, number, reason) → stored on the call → event to Textify.FR-A7: Barge-in/interruptions and turn detection are handled by LiveKit Agents.
8.4 Human agents (browser)
- FR-H1: Agents are
identifiedonlybyan opaqueagentReffrom+Textify.display name in the engine. Textify owns theuser, login and role.user. - FR-H2:
Agent sets statusStatusAVAILABLE/BUSY/OFFLINE.throughOnlineTextify,presencewhichisforwardsautomaticit towhile theVoiceagent'sService.browser is connected. - FR-H3:
IncomingRingcallpopup→+available agentsringtone in thegroup get abrowser;ringfirst to accept wins;inothers'thepopupsbrowser (data message over LiveKit). Accept → join the call room. Reject or timeout → next.close. - FR-H4: Ring
strategy:strategy per group:ALL_AT_ONCE(default) orROUND_ROBIN. - FR-H5:
Agent can mute, hold,Mute, hang up,andtransfer to anotheragent.agent or group. - FR-H6:
Click-to-calloutbound from the dashboard: agent clicks a number → Voice Service dials out via the trunk → agent and customer in one room. FR-H7: FallbackOptional transfer to an external mobileis allowed(costs about TZS 60/min). Offoff bydefault.default; extra cost).
8.4 Outbound: click-to-call
- FR-O1: Agent clicks a number in Textify → engine places the call via AT from the account's number (caller ID = that number).
- FR-O2: Agent is already in the call room in the browser and hears ringing. Customer answers → connected.
- FR-O3: Balance check before dialing.
8.5 Outbound: broadcast campaigns
- FR-C1: Textify creates a campaign: account, from-number, audio (uploaded file URL) or TTS text, recipients list, start time, calling window (default 08:00–20:00 EAT), max calls per second / concurrent, retry policy (default 2 retries, 30 min apart, for no-answer/busy).
- FR-C2: Optional keypad options after the message:
1→ talk to agents (ring group),9→ opt out. (Phase 3:1→ AI.) - FR-C3: Opt-out / do-not-call list per account, always checked before dialing.
- FR-C4: Pause / resume / cancel any time (Textify pauses automatically when the wallet runs low).
- FR-C5: Per-recipient result:
ANSWERED(seconds, keypress),NO_ANSWER,BUSY,FAILED,OPTED_OUT. - FR-C6: Progress events (batched) + completion event. Textify can SMS everyone who didn't answer (key selling point).
- FR-C7: Throughput limited by AT's calls-per-second / concurrent-call limits for our account (confirm).
8.6 Recording, transcription, summary
- FR-R1: Recording
on/offpernumbernumber/campaign (default on,for agent calls, withconsentnotice). - FR-R2:
AfterEnginethedownloadscall:everyaudiorecordingfileintostored inourobjectstorage(never rely on provider links).immediately. - FR-
R3:R3Transcription(Phase 2): transcript (Swahili/English)→+ summarycontaining:JSON:- 2–3 line
summary customersummary, intent,resolvedresolved,yes/nofollow-ups, follow-upsentiment,actionssentiment (positive / neutral / negative)topic tag (sales, support, complaint, inquiry, other)
topic. - 2–3 line
- FR-R4:
TranscriptRetention+ summary attached to the call record; eventsummary.readysent to the client app. FR-R5: Retention: recordings deleted afterdefault 90 daysby(per-accountdefault (configurable per account)setting). Tanzania Personal Data Protection Act applies.FR-R6: Summary is a billable add-on (Textify decides price; Voice Service reportssummaryMinutes).
8.6 Missed calls & follow-ups
FR-M1: A call isMISSEDif no human or AI handled it (e.g. HUMAN_ONLY with nobody answering and no fallback, or the caller hung up while ringing).FR-M2: Voice Service emitscall.missedwith the caller number.Textifysends the SMS/WhatsApp follow-up ("Samahani tumekukosa, tutakupigia tena"; in English: "Sorry we missed you, we'll call you back") using its existing messaging.
8.7 Usage & billing
- FR-B1: On
everycallend,end theVoice Serviceengine emitsusage:call.endedwithdirection,usagesecondsbroken down byper leg (seconds):inboundSeconds,,outboundSecondspstnSecondstrunkSeconds,aiSeconds), AT's reported,externalTransferSecondsamountrecorded,recorded(bool),summarySeconds.summary. - FR-B2: Textify prices and
deducts from the wallet.deducts. TheVoice Serviceengine neverdeals withhandles money. - FR-B3: Events are idempotent (event id),
andsigned, retried untilTextify returns2xx.
8.8 Dashboard (built in Textify,Textify; data from the Voice Service)engine)
Call
log:log,time,calldirection,detailcaller,(audio,number,transcript,handledsummary), number + flow editor, agents and status, ring groups, campaigns (progress, results, "SMS non-answerers"), simple stats (answered/missed, by(AIagent,/ agent), duration, status, cost (Textify-computed).Call detail: audio player, transcript, summary, AI message taken.Number settings:campaign answermode, greeting, AI instructions, recording on/off, ring group, timeout, fallback.Agents: availability, calls handled.Simple stats: calls per day, answered vs missed, AI-handled vs human, average duration.
9. Non-functional requirements
| Area | Requirement |
|---|---|
| Availability | |
| Isolation | Dedicated |
| Security | |
| Latency | Callback XML response < 500 ms; call reaches agent browser ring < 3 s |
| Privacy | |
| Simplicity | REST + webhooks + Postgres + Redis + LiveKit. No broker, no OAuth, no approval |
10. Phases
Phase 0: Proof of concept (goal: one real call end-to-end)
Confirm withWorking AT(see §11). Get one TZnumber+(done)SIP→trunk.set callback URL to the engine →Saygreeting works.DedicatedDialfrom the XML → caller lands in LiveKitbox:sip:{callId}@<livekit-sip-host>serverroom+ LiveKit SIP + Redis. Firewall restricted to AT.call-{callId}.InboundBrowsertrunktest+pagedispatch rule → Spring Boot dials OpenAI Realtime intojoins the roomover→SIPtwo-way audio with the caller.- Check AT final callback: duration, amount, recording URL.
- Outbound: AT call API →
AIanswergreets→inDialSwahili.into Measure:LiveKitlatency,→audiobrowserquality, OpenAI cost per minute.Swahili test: 10–20 real people (Swahili, English, mixed). Judge understanding and voice. Swap TTS ifhears thevoice is weak.customer.
Phase 1: Call center MVP
(sellable)
Spring Boot Voice Service: accounts,Accounts, numbers,config,flowscalls, events, LiveKit integration.AI receptionist with handover to(say/play/menu/ring/voicemail/hangup), browseragents.Browser agent clientagents (inlobby,Textify'sring,web app) +accept), ring groups + waiting, click-to-call.call, Recordingrecording to our storage, missed-call events, usage events, balance check.Phase 2: Bulk +
storageintelligence Broadcast campaigns (throttle, window, retries, opt-out, press-1-to-agent, results, SMS fallback via Textify), OpenAI transcription +retention.summaries, Balancebusinesscheckhours,+stats.Phase 3: AI
call.endedaiusagefloweventsnode: OpenAI Realtime joins the LiveKit room over SIP, controlled from Java; AI →wallethumandeductionhandover;in Textify.Missed-call SMS/WhatsApp.
Phase 2
Transcripts + AI summaries (if not already in MVP).Account-defined AI tools (webhooks to the client app: check order, booking).Voicemail, business hours / after-hours behaviour.Stats dashboard.
Phase 3
Voice broadcast(recorded message to a contact list: "bulk voice", fits Textify's bulk DNA). Check TZ calls-per-second limits.- AI outbound campaigns (
remindersreminders,thatconfirmations,talksurveys).back).Swahili - quality test with real callers first.
White-Later: partner white-labelvoicevoice,forotherpartners.countries, Other countries (Uganda/Nigeria) through AT.Otherother QBIT products asclientsengineof the Voice Service.
11. Open questionsQuestions for Africa's Talking (must answer before Phase 1)
- Can
theXMLTZDialSIP trunkroute inbound calls for our number totarget an external SIP URI with a custom user part (our LiveKit SIP endpoint)sip:{callId}@our-host)?Note:Transport,LiveKitport,SIPcodecsdoesnot support SIP REGISTER(G.711),soDTMFAT must send calls to us directly.method? Can itaccept outbound calls from our server IP(and what auth: IP allowlist or username/password)?What areAT's SIP signallingand+ mediaIP addressesIPs (for our firewall and LiveKit allowlist).- Billing for Dial-to-SIP: is it 17 (or 60 outbound) + 8.61 trunk? Anything else?
- Does
Dial record="true"record a Dial to SIP, and what's the recording format and retention on AT's side? - Calls-per-second and max concurrent calls (inbound + outbound) for our account; can they be raised?
- Outbound billing per second or per started minute? Are unanswered/busy calls free?
- Can the outbound call API take multiple numbers per request? What does it return (session ids)?
ExactDobillingvoiceforcallbacks come from fixed IPs? Is there anyinboundcallbackto trunksignature: 17 + 8.61? Any extra??Exact billing foroutbound via trunk: 60 + 8.61? Any extra?Is theSIP agent TZS 4.30charged on inbound calls too? Any setup/monthly fee for SIP agents?Areunanswered ringsto SIP endpoints billed? 7b.Thelistedregular number is Airtel.:Howanydoissues for callersonfrom Vodacom, Yas,HalotelHalotel,and TTCL reach it, and at what cost to them and to us?TTCL?Calls-per-secondTCRA:(CPS)limits and pricing in Tanzania (needed for broadcast).Supportedcodecs(G.711 PCMU/PCMA expected) and DTMF method (RFC 2833/4733).Doesdoes reselling voiceto our customersneedanyTCRAa licence on ourside,side?orRulesisforAT'sautomated/marketinglicencecallsenough?(consent,Gethours,itcallerinID)? In writing.Number porting / getting several numbers quickly; toll-free availability.
AT is not on LiveKit's list of tested SIP providers (Twilio, Telnyx, Exotel, Plivo, Wavix, Sinch, didlogic). LiveKit is designed to work with any SIP provider, so the PoC must prove compatibility.
12. Risks
| Risk | Mitigation |
|---|---|
| AT |
|
| AT throughput limits slow broadcasts | Confirm Q5; engine throttles; show ETA in Textify |
| Swahili |
|
| SIP |
Firewall + LiveKit allowlist to AT IPs |
| Textify outage |
Fail-open balance check; |
| Written |
|
| Single box |
Fine for Phase |
13. Decisions log
| # | Decision |
|---|---|
| D1 | |
| D2 | |
| D3 | |
| D4 | Any call that needs a person (or AI) is Dial |
| D5 | |
| D6 | Three adapters inside the engine: |
| D7 | |
| D8 | |
| D9 | |
| D10 | Billing per minute / per answered call from the Textify wallet; engine reports usage only |
| D11 | Dedicated Voice box, separate from NexGate's LiveKit |
| D12 | No approval workflows |