Skip to main content

Textify 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

  1. Customers get a number, choose how it answers (Direct, Menu, later AI), and calls reach agents answering in the browser.
  2. Callers waiting for an agent hear hold music, never silence; nobody waits forever (voicemail).
  3. Click-to-call outbound for agents.
  4. Broadcast campaigns: voice message to thousands, keypad actions (talk to us, confirm via webhook, opt out…), retries, SMS fallback for non-answerers.
  5. Customers create what callers hear by uploading/recording audio or typing text (Basic or Premium voice).
  6. Keypresses can trigger the customer's own systems (webhooks with parameters).
  7. Every call is logged, optionally recorded, and (Phase 2) transcribed + summarized.
  8. Missed calls / voicemails become follow-ups in Textify (tickets, SMS/WhatsApp).
  9. Predictable costs: per-minute call billing from the Textify wallet; voice-generation cost known before it's spent.
  10. (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: connect to 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: agentRef only in the engine; status AVAILABLE / BUSY / OFFLINE; presence automatic while the browser is connected.
  • FR-I8: Ring strategy per department: ALL_AT_ONCE (default) or ROUND_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: webhook node 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 ai node 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_knowledge tool 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, AT amount, 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)

  1. Callback → Play a file works.
  2. GetDigits menu works; keys arrive.
  3. Wait loop: GetDigits wrapping Play hold segment, callback every segment.
  4. Dial sip:{callId}@<livekit-sip-host> → room call-{callId} → browser test page hears the caller.
  5. Final callback fields (duration, amount, recordingUrl).
  6. Outbound call API → answer → Dial into LiveKit.
  7. Voice listening test: same Swahili sentences in Basic candidate (Piper), Premium candidates (OpenAI, Azure), and AT Say for 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)

  1. Can XML Dial target an external SIP URI with a custom user part (sip:{callId}@our-host)? Transport, port, codecs (G.711), DTMF method?
  2. AT SIP signalling + media IPs (firewall, LiveKit allowlist).
  3. Billing for Dial-to-SIP: 17 (or 60 outbound) + 8.61? Anything else?
  4. Does Dial record="true" record Dial-to-SIP legs? Format, how long AT keeps the file?
  5. CPS and max concurrent calls (in + out) for our account; can they be raised?
  6. Outbound billed per second or per started minute? Unanswered/busy free?
  7. Outbound call API: multiple numbers per request? Returned session ids?
  8. Callback source IPs; any signature on callbacks?
  9. GetDigits wrapping Play: keys interrupt playback? Max timeout? Any limit on how many callbacks per call (our wait loop)?
  10. Play: timeout for fetching the URL; does it stream while downloading; any caching/pre-upload of media?
  11. After a Dial ends unanswered, does AT continue to the next XML (callback after Dial)?
  12. Airtel number: any issues for callers from Vodacom, Yas, Halotel, TTCL?
  13. 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