Skip to main content

Textify Voice: Product & Functional Specification

Status: draft for implementation · Owner: Joshua Saimon Sakweli (QBIT SPARK) · Updated 2026-11-08 Companion: 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 center as a new Textify module, so the same Textify customers can use it from the same portal and wallet. The product is similar in spirit to SemaCall (semacall.com: "One business. One number. Every team"), plus Textify's own strength, bulk: voice broadcast with SMS fallback.

Voice is built as a separate service, the Voice Engine (Java Spring Boot). 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 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. A Textify customer gets a number with a greeting + keypad menu, and calls reach human agents answering in the browser.
  2. Outbound: agents make click-to-call calls from the browser.
  3. Broadcast: send a recorded voice message to thousands of numbers, with "press 1 to talk to us", retries, opt-out, and SMS fallback for people who didn't answer.
  4. Every call is logged, optionally recorded, and (Phase 2) transcribed + summarized by AI.
  5. Missed calls → automatic SMS/WhatsApp from Textify.
  6. Billing per minute / per answered call from the existing Textify wallet.
  7. (Phase 3) AI receptionist and AI outbound calls on the same engine.

3. Non-goals

  • No users, auth, roles, plans or prices in the engine.
  • No approval workflows anywhere. Everything self-service.
  • No video over phone lines.
  • No message broker (Kafka/RabbitMQ/MQTT) in v1 (see ARCHITECTURE §7).
  • 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 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 Protocol internet phones use to set up calls. Audio travels separately (RTP).
SIP trunk Connection carrying many calls between two systems. Here: AT → our LiveKit.
LiveKit room A live audio session. Each answered call = one room; the caller and agents (and later AI) are participants.
Browser agent Staff answering calls in the Textify web app (LiveKit JS SDK).
Flow What happens when a number rings: greeting → menu → route. Stored as JSON.
Endpoint Something that can take a call: ring group of agents, voicemail, AI (later).
Campaign A broadcast: one audio message to a list of numbers.
DTMF Keypad presses ("press 1").

5. Providers

Provider Role
Africa's Talking Numbers, PSTN both directions, voice callbacks/XML, outbound call API, call recording, SIP trunk to LiveKit
LiveKit (self-hosted, dedicated box) Rooms + browser agents (WebRTC), hold audio, (Phase 3) AI participant. We already run LiveKit for NexGate; Voice gets its own box
OpenAI Phase 2: transcription + summaries (REST). Phase 3: live AI voice (Realtime over SIP, controlled from Java)

6. 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 number Same + TZS 170,000 deposit
Incoming TZS 17 / min
Outgoing TZS 60 / min
Toll-free incoming TZS 200 / min
SIP trunk TZS 8.61 / min
SIP agent TZS 4.30 / min (not used in our design)

How our calls combine:

Call type Approx. TZS / min
Inbound, only greeting/menu/voicemail (no agent) 17
Inbound answered by browser agent (via LiveKit) 17 + 8.61 ≈ 25.61
Click-to-call outbound (agent in browser) 60 + 8.61 ≈ 68.61
Broadcast, message only 60 per answered minute (unanswered likely free, confirm)
Broadcast, caller presses 1 → agent 60 + 8.61 for the talking part

Agreed facts:

  • Cost follows minutes, not the number of agents. Unlimited agents per account.
  • No forwarding to mobiles by default (17 + 60 = 77/min).
  • AI (Phase 3) adds OpenAI cost per minute. Measure in the PoC before pricing.

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; AI minute 150–250; summary add-on TBD. Prices live in Textify only.

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 & flows

  • FR-N1: Ops registers AT numbers in the engine; Textify assigns a number to an account.
  • FR-N2: Each number has one flow (JSON, built in Textify's UI, stored and executed by the engine).
  • FR-N3: Flow node types (keep small): say, play, menu (GetDigits with options), ring (ring group), voicemail, hangup. Phase 3: ai.
  • FR-N4: Flow options: business hours (open/closed branch), recording on/off, ring timeout, no-answer action.
  • FR-N5: Saving a flow takes effect on the next call. No approval.

8.2 Inbound

  • FR-I1: AT callback → engine looks up number → account → flow.
  • FR-I2: Balance check: engine asks Textify "can this account take a call?". Textify unreachable → allow (fail-open).
  • FR-I3: No balance → short message, hang up.
  • FR-I4: 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: Greeting and menu run on AT XML (no LiveKit cost).
  • FR-I6: ring node → call is dialed into LiveKit → caller hears hold audio → engine rings the group's available agents in their browsers.
  • FR-I7: Waiting: if all agents are busy, the caller stays on hold and the engine rings again as agents become free (engine-side queue), until the timeout.
  • FR-I8: Timeout → noAnswer action: voicemail, hangup, (Phase 3) AI.
  • FR-I9: Not handled by anyone → MISSED → event → Textify sends the SMS/WhatsApp follow-up.

8.3 Browser agents

  • FR-H1: Agents are only an opaque agentRef + display name in the engine. Textify owns the user.
  • FR-H2: Status AVAILABLE / BUSY / OFFLINE. Online presence is automatic while the agent's browser is connected.
  • FR-H3: Ring popup + ringtone in the browser; first to accept wins; others' popups close.
  • FR-H4: Ring strategy per group: ALL_AT_ONCE (default) or ROUND_ROBIN.
  • FR-H5: Mute, hang up, transfer to another agent or group.
  • FR-H6: Optional transfer to an external mobile (off by 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 per number/campaign (default on for agent calls, with notice).
  • FR-R2: Engine downloads every recording into our storage immediately.
  • FR-R3 (Phase 2): transcript (Swahili/English) + summary JSON: 2–3 line summary, intent, resolved, follow-ups, sentiment, topic.
  • FR-R4: Retention default 90 days (per-account setting). Tanzania Personal Data Protection Act applies.

8.7 Usage & billing

  • FR-B1: On call end the engine emits usage: direction, seconds per leg (pstnSeconds, trunkSeconds, aiSeconds), AT's reported amount, recorded, summary.
  • FR-B2: Textify prices and deducts. The engine never handles money.
  • FR-B3: Events are idempotent (event id), signed, retried until 2xx.

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 work if Textify is down (fail-open balance check, queued events)
Isolation Dedicated Voice box, not shared with NexGate's LiveKit
Security SIP open only to AT; API keys + HMAC between services; signed/verified webhooks everywhere (ARCHITECTURE §8)
Latency Callback XML response < 500 ms; call reaches agent browser ring < 3 s
Privacy Recording notice, retention, our own storage, signed URLs
Simplicity REST + webhooks + Postgres + Redis + LiveKit. No broker, no OAuth, no approval flows

10. Phases

Phase 0: Proof of concept

  1. Working AT number (done) → set callback URL to the engine → Say greeting works.
  2. Dial sip:{callId}@<livekit-sip-host> from the XML → caller lands in LiveKit room call-{callId}.
  3. Browser test page joins the room → two-way audio with the caller.
  4. Check AT final callback: duration, amount, recording URL.
  5. Outbound: AT call API → answer → Dial into LiveKit → browser hears the customer.

Phase 1: Call center MVP Accounts, numbers, flows (say/play/menu/ring/voicemail/hangup), browser agents (lobby, ring, accept), ring groups + waiting, click-to-call, recording to our storage, missed-call events, usage events, balance check.

Phase 2: Bulk + intelligence Broadcast campaigns (throttle, window, retries, opt-out, press-1-to-agent, results, SMS fallback via Textify), OpenAI transcription + summaries, business hours, stats.

Phase 3: AI ai flow node: OpenAI Realtime joins the LiveKit room over SIP, controlled from Java; AI → human handover; AI outbound campaigns (reminders, confirmations, surveys). Swahili quality test with real callers first.

Later: partner white-label voice, other countries, other QBIT products as engine 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's SIP signalling + media IPs (for our firewall and LiveKit allowlist).
  3. Billing for Dial-to-SIP: is it 17 (or 60 outbound) + 8.61 trunk? Anything else?
  4. Does Dial record="true" record a Dial to SIP, and what's the recording format and retention on AT's side?
  5. Calls-per-second and max concurrent calls (inbound + outbound) for our account; can they be raised?
  6. Outbound billing per second or per started minute? Are unanswered/busy calls free?
  7. Can the outbound call API take multiple numbers per request? What does it return (session ids)?
  8. Do voice callbacks come from fixed IPs? Is there any callback signature?
  9. The regular number is Airtel: any issues for callers from Vodacom, Yas, Halotel, TTCL?
  10. TCRA: does reselling voice need a licence on our side? Rules for automated/marketing calls (consent, hours, caller ID)? In writing.

12. Risks

Risk Mitigation
AT can't Dial to our SIP URI Phase 0 test step 2 first. Fallback: AT SIP numbers on softphones for agents (no LiveKit)
AT throughput limits slow broadcasts Confirm Q5; engine throttles; show ETA in Textify
Swahili AI quality (Phase 3) Real-caller test; AI only where it passes; human fallback
SIP attacks Firewall + LiveKit allowlist to AT IPs only
Textify outage Fail-open balance check; outbox retries
Regulation (TCRA, PDPA) Written answers; notices; opt-out; retention
Single box Fine for Phase 1–2; LiveKit/engine can scale out with Redis later

13. Decisions log

# Decision
D1 Engine model: separate Java Spring Boot Voice Engine, blind to users/auth/money; Textify (Go) consumes it via REST + webhooks
D2 AT is the only PSTN path, both directions (inbound callbacks; outbound call API)
D3 Greetings/menus/voicemail/broadcast audio run on AT XML (no LiveKit cost)
D4 Any call that needs a person (or AI) is dialed into LiveKit via Dial sip:{callId}@livekit
D5 All agents answer in the browser via LiveKit (no AT SIP softphones, no mobile forwarding by default)
D6 Three adapters inside the engine: Telephony (AT), Media (LiveKit), AI (OpenAI), plain Java interfaces, one implementation each
D7 No Python: AI later via OpenAI Realtime over SIP, controlled from Java. Python LiveKit worker only as fallback
D8 Service auth: API key per client app (in), HMAC-signed webhooks (out), secret URL + IP allowlist for AT callbacks, built-in signatures for LiveKit/OpenAI
D9 Communication: REST + webhooks + Postgres outbox/job tables + Redis + LiveKit data messages. No Kafka/RabbitMQ/MQTT in v1
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