A USSD menu is a small web service. Every time a customer dials your code or picks an option, the network sends a request to your server, and your server replies with the next screen. If you can build a simple API endpoint, you can build a USSD application. This guide shows how USSD sessions reach your server in Kenya, the exact request and reply format on the Brilio USSD API, and working examples in Python, Node.js and PHP.
Short answer: you build one HTTPS endpoint (your callback URL). For each step of a session it receives a JSON request with the customer's number and everything they have typed so far, and it replies with JSON: the text to show and
CON(wait for another input) orEND(close the session). You can test it in our USSD simulator without a SIM card, then go live on a USSD code.
How a USSD session reaches your server
- A customer dials your code, for example
*711*7777#. - The mobile network passes the session to the USSD gateway.
- The gateway sends a POST request to your callback URL with the session details.
- Your server replies with the screen to show.
- When the customer answers, steps 3 and 4 repeat, until you reply with
END.
The customer is looking at a loading screen while your server works, so answer each request quickly, ideally in well under two seconds.
The request your server receives
| Field | Example | Meaning |
|---|---|---|
sessionId |
"ATUid_8f2e1c" |
The same for every step of one session |
msisdn |
254712345678 |
The customer's phone number |
code |
"*711*7777#" |
The code they dialled |
level |
3 |
Step number: 1 when they dial, then +1 for each reply |
input |
"2" |
What they just entered (empty on the first step) |
text |
"1*2" |
Everything entered this session, joined with * (empty on the first step) |
network |
"Safaricom KE" |
The customer's network |
Every request also carries an Authentication header with your callback secret, so your server can reject
requests that did not come from the gateway.
The reply your server sends
{"text": "Welcome to Jamii SACCO\n1. Check balance\n2. Apply for a loan", "responseType": "CON"}
responseType: "CON"shows the text and waits for the customer's next input.responseType: "END"shows the text and closes the session.- Keep
textshort (under about 160 characters) and use\nfor new lines.
The example menu
We will build a small SACCO menu:
- 1. Check balance → shows the balance and ends.
- 2. Apply for a loan → asks for an amount, then confirms and ends.
The trick is the text field: split it on * and you have every answer in order. "" means the first screen,
"2" means they chose option 2, and "2*5000" means they chose 2 and then typed 5000.
Python (Flask)
from flask import Flask, jsonify, request
app = Flask(__name__)
CALLBACK_SECRET = "your-callback-secret"
def reply(text, response_type):
return jsonify({"text": text, "responseType": response_type})
@app.post("/ussd")
def ussd():
if request.headers.get("Authentication") != CALLBACK_SECRET:
return jsonify({"error": "unauthorised"}), 401
data = request.get_json()
steps = data["text"].split("*") if data["text"] else []
if not steps:
return reply("Welcome to Jamii SACCO\n1. Check balance\n2. Apply for a loan", "CON")
if steps[0] == "1":
return reply("Your savings balance is KES 12,450.", "END")
if steps[0] == "2" and len(steps) == 1:
return reply("Enter the loan amount in KES:", "CON")
if steps[0] == "2":
return reply(f"Loan request for KES {steps[1]} received. We will SMS you shortly.", "END")
return reply("Invalid choice. Please dial again.", "END")
Node.js (Express)
const express = require("express");
const app = express();
app.use(express.json());
const CALLBACK_SECRET = "your-callback-secret";
const reply = (res, text, responseType) => res.json({ text, responseType });
app.post("/ussd", (req, res) => {
if (req.get("Authentication") !== CALLBACK_SECRET) {
return res.status(401).json({ error: "unauthorised" });
}
const steps = req.body.text ? req.body.text.split("*") : [];
if (steps.length === 0) return reply(res, "Welcome to Jamii SACCO\n1. Check balance\n2. Apply for a loan", "CON");
if (steps[0] === "1") return reply(res, "Your savings balance is KES 12,450.", "END");
if (steps[0] === "2" && steps.length === 1) return reply(res, "Enter the loan amount in KES:", "CON");
if (steps[0] === "2") return reply(res, `Loan request for KES ${steps[1]} received. We will SMS you shortly.`, "END");
return reply(res, "Invalid choice. Please dial again.", "END");
});
app.listen(3000);
PHP
<?php
$secret = "your-callback-secret";
header("Content-Type: application/json");
if (($_SERVER["HTTP_AUTHENTICATION"] ?? "") !== $secret) {
http_response_code(401);
echo json_encode(["error" => "unauthorised"]);
exit;
}
$data = json_decode(file_get_contents("php://input"), true);
$steps = $data["text"] === "" ? [] : explode("*", $data["text"]);
function reply($text, $type) {
echo json_encode(["text" => $text, "responseType" => $type]);
exit;
}
if (count($steps) === 0) reply("Welcome to Jamii SACCO\n1. Check balance\n2. Apply for a loan", "CON");
if ($steps[0] === "1") reply("Your savings balance is KES 12,450.", "END");
if ($steps[0] === "2" && count($steps) === 1) reply("Enter the loan amount in KES:", "CON");
if ($steps[0] === "2") reply("Loan request for KES {$steps[1]} received. We will SMS you shortly.", "END");
reply("Invalid choice. Please dial again.", "END");
Test it without a SIM card
Before going live, test every path in the USSD simulator in your Brilio portal. It sends your server exactly the same requests a real phone would, so you can fix problems before a customer sees them:
Six rules for a USSD menu people finish
- Answer fast. Every step, the customer is staring at a loading screen. Keep database calls light and avoid slow third-party calls in the middle of a session.
- Most-used option first. Order options by how often people choose them.
- Short screens. Four or five options, short words, one question per screen.
- Validate input. If someone types letters for an amount, say so and ask again rather than failing.
- End with an SMS. Close the session with a short message and send details or receipts by SMS, which the customer can keep.
- Log every step. Your Brilio portal shows each session step by step with response times, so you can see where people drop off.
Related guides: How to apply for a USSD code · USSD code cost in Kenya · USSD charges: who pays per session
Frequently asked questions
What programming language can I use to build a USSD application?
Any language that can run a web server and return JSON: Python, Node.js, PHP, Java, Go, C# and others. The gateway only needs an HTTPS URL that answers POST requests.
How do I know which option the customer chose?
Use the text field. It holds everything the customer has entered in the session, joined with *, so
"2*5000" means they chose option 2 and then entered 5000. The input field holds just the latest entry.
How fast must my USSD server respond?
As fast as possible. Customers wait on a loading screen for every step, and slow replies make people give up. Aim for well under two seconds per step.
Can I test a USSD menu without a phone?
Yes. The USSD simulator in the Brilio portal sends your callback the same requests as a live session, so you can test every path before you get a code.
Do I need my own USSD code to start building?
No. Build and test with the simulator first. When you are ready, apply for a shared or dedicated code and point it at the same callback URL.
The full request and reply reference is in our developer documentation.