USSD Runtime
Run the USSD menus you design in USSD Flow Studio, fast enough for a real phone session.
What it does
An open-source Go service that runs USSD menu flows on Africa's Talking, Hubtel, Nalo and Arkesel. It runs USSD Flow Studio exports unchanged, and adds typed answers with validation, variables in prompts, and signed calls to your backend with a hard time limit. Sessions live in Redis, so several instances can share the work.
The problem
USSD still reaches people without smartphones or data, but every network gives you only a few seconds per reply, and every gateway speaks a different format. Slow servers mean dropped sessions and lost customers.
What you get



How to use it
Design your menu in USSD Flow Studio (or write the JSON), run it with the runtime, and point your gateway's callback URL at it. Add input screens and actions when you need typed answers or data from your own system.
1. Run it with the simulator
git clone https://github.com/Jacksonoseikojo1122/ussd-runtime.git
cd ussd-runtime
FLOW_FILE=flows/demo-wallet.json SIMULATOR=on go run ./cmd/ussd-runtime
# open http://localhost:8080 and press Dial2. Point your gateway at it
Africa's Talking https://<your-host>/ussd/africastalking?token=<GATEWAY_TOKEN>
Hubtel https://<your-host>/ussd/hubtel?token=<GATEWAY_TOKEN>
Nalo https://<your-host>/ussd/nalo?token=<GATEWAY_TOKEN>
Arkesel https://<your-host>/ussd/arkesel?token=<GATEWAY_TOKEN>3. Ask for a typed answer and call your backend
"pay_student": {
"prompt": "Enter the student ID", "kind": "input",
"input": { "name": "student_id", "type": "digits", "min_length": 4, "next": "pay_lookup" }
},
"pay_lookup": {
"prompt": "Pay for {{student_name}}?\nOutstanding: GHS {{balance}}", "kind": "menu",
"action": { "url": "$BACKEND_URL/ussd/student", "timeout_ms": 2000, "can_go_to": ["not_found"] },
"options": [{ "key": "1", "label": "Yes", "next": "pay_amount" }]
}4. Answer the action from your backend (Node.js)
app.post("/ussd/student", express.raw({ type: "application/json" }), (req, res) => {
if (!validSignature(req)) return res.status(401).end(); // X-USSD-Signature, see docs/actions.md
const { vars } = JSON.parse(req.body.toString());
const s = findStudent(vars.student_id);
if (!s) return res.json({ next: "not_found" });
res.json({ vars: { student_name: s.name, balance: s.balance } });
});Documentation
- Getting started Run the demo, your Flow Studio export, and a flow with a backend
- Flow format Screens, input types, variables, actions and validation
- Actions The backend contract and signature checks in Node, Python, PHP and Go
- Gateways Africa's Talking, Hubtel, Nalo and Arkesel formats, with sources
- Operations Load-test results, Redis, Docker, Kubernetes, metrics and alerts
- OpenAPI spec Every endpoint, plus the action webhook
Features
- Adapters for Africa's Talking, Hubtel, Nalo and Arkesel, built from each provider's documentation
- Runs USSD Flow Studio exports unchanged: the Studio's engine tests run against it in CI
- Input screens for text, digits, amounts and Ghana phone numbers, checked before your backend sees them
- Signed backend calls with a 2-second budget and an allow-list of screens the backend can send callers to
- Sessions in Redis (several instances) or memory (one), with a phone simulator for testing
- Prometheus metrics and a load-test tool with published results
What building it covers
- Go
- Low-latency services
- Load testing
- Session state at scale
Stack
Questions
Which USSD gateways does it support?
Africa's Talking, Hubtel, Nalo and Arkesel. Each has its own endpoint, /ussd/africastalking, /ussd/hubtel, /ussd/nalo and /ussd/arkesel, and the runtime answers in that gateway's format.
Can I run a flow I designed in USSD Flow Studio?
Yes. Export the flow as JSON and set FLOW_FILE to it. USSD Flow Studio's own engine tests run against the runtime in CI, so callers get the same replies the Studio's simulator shows.
How do I ask the caller to type something, like an amount or a phone number?
Use an input screen with type text, digits, amount or phone_gh. The answer is checked (for example an amount between GHS 1 and GHS 5,000), stored as a variable and can be shown in later prompts as {{amount}}.
How does it get data from my system?
Add an action to a screen. When a caller reaches it, the runtime POSTs the session to your URL, signed with HMAC-SHA256, and waits up to 2 seconds. Your backend returns variables to show, or one of the screens you allowed it to send the caller to.
How fast is it?
On a laptop, with 50 callers going through a payment flow that calls a backend twice, it handled 14,000 to 19,000 requests a second with a p99 of about 10 ms. CI runs the same test with Redis on every change.
Do I need Redis?
Only if you run more than one instance. Without REDIS_URL, sessions are kept in memory.
Is it free?
Yes. It's open source under the MIT licence. You still need a USSD code from a gateway such as Africa's Talking, Hubtel, Nalo or Arkesel.