Build a USSD app in Kenya by getting a USSD shortcode through Africa's Talking, setting up a server endpoint that receives session requests, managing menu navigation through session state, and responding with text menus the user interacts with by pressing number keys. USSD is session-based and text-only, so design for simplicity and speed.
USSD stands for Unstructured Supplementary Service Data. If you have ever dialed *144# on Safaricom to check your M-Pesa balance, you have used USSD. It is a protocol built into the GSM standard, which means it works on every phone with a SIM card: smartphones, feature phones, and even the Nokia 3310 your grandmother still uses.
Here is what makes USSD different from the technologies you are probably used to building with:
Why USSD still matters in Kenya in 2026. Kenya has strong smartphone penetration in urban areas, but step outside Nairobi, Mombasa, and Kisumu and the picture changes. Millions of Kenyans still use feature phones. Others have smartphones but cannot afford consistent data bundles. M-Pesa itself is a USSD application, and it processes billions of shillings daily. If M-Pesa runs on USSD, your service can too.
The use cases where USSD makes sense: checking account balances, making payments, receiving one-time passwords, taking surveys, placing simple orders, and any interaction that takes less than 60 seconds. If the interaction is complex (browsing a product catalog, filling out a long form), USSD is the wrong channel. But for quick, essential transactions, nothing else has the same reach.
To build a USSD app, you need two things: a USSD shortcode (the number users dial) and a server that handles the session. Getting a shortcode directly from Safaricom, Airtel, or Telkom is possible but involves paperwork, telecom agreements, and significant lead time. Africa's Talking simplifies this dramatically.
Africa's Talking is a Nairobi-based API company that provides USSD, SMS, voice, and payment APIs for African developers. They handle the telecom relationships so you do not have to. For USSD specifically, they give you a sandbox shortcode for testing and help you get a production shortcode when you are ready to go live.
Step 1: Create an account. Go to africastalking.com and sign up. The sandbox is free. You get a test shortcode and a simulator that lets you test your USSD flow without needing a real phone.
Step 2: Set up a sandbox app. Once logged in, go to the sandbox dashboard. You will see your sandbox API key and a default USSD shortcode (usually something like *384*123# in sandbox). Under the USSD section, you need to set a callback URL. This is the URL of your server endpoint that Africa's Talking will send requests to when a user dials your code.
Step 3: Set up your server. Your server needs a single POST endpoint that receives USSD session data and returns a text response. Africa's Talking sends the following parameters with each request:
sessionId: A unique identifier for this USSD sessionserviceCode: The USSD code the user dialedphoneNumber: The user's phone numbertext: The user's input so far, with each response separated by asterisks (e.g., "1*2*John" means the user selected option 1, then option 2, then typed "John")Step 4: Respond correctly. Your response is plain text. If the response starts with CON, the session continues and the user can reply. If it starts with END, the session closes after displaying the message. That is literally the entire protocol. CON to continue, END to finish.
Here is a minimal Node.js example with Express:
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: false }));
app.post('/ussd', (req, res) => {
const { sessionId, serviceCode, phoneNumber, text } = req.body;
let response = '';
if (text === '') {
// First request: show the main menu
response = 'CON Welcome to MyApp\n';
response += '1. Check Balance\n';
response += '2. Make Payment\n';
response += '3. Help';
} else if (text === '1') {
response = 'END Your balance is KES 1,200';
} else if (text === '2') {
response = 'CON Enter amount to pay:';
} else if (text.startsWith('2*')) {
const amount = text.split('*')[1];
response = `END Payment of KES ${amount} initiated. You will receive a confirmation SMS.`;
} else {
response = 'END Invalid option. Please try again.';
}
res.set('Content-Type', 'text/plain');
res.send(response);
});
app.listen(3000);
That is a working USSD application. It is simple, but it demonstrates the core pattern: read the text parameter to figure out where the user is in the menu tree, and return the appropriate response.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
The text parameter from Africa's Talking accumulates the user's responses throughout the session, separated by asterisks. When the user first dials, text is an empty string. When they select option 1, text becomes "1". When they then type "500", text becomes "1*500". This gives you the user's entire navigation path in a single string.
For simple menus, parsing the text string with split('*') and checking the length and values is sufficient. But as your menu tree grows, this approach becomes a tangled mess of if/else statements. Here is a cleaner pattern using a level-based approach:
app.post('/ussd', (req, res) => {
const { text, phoneNumber } = req.body;
const inputs = text === '' ? [] : text.split('*');
const level = inputs.length;
let response = '';
if (level === 0) {
response = 'CON Main Menu\n1. Account\n2. Payments\n3. Support';
} else if (level === 1 && inputs[0] === '1') {
response = 'CON Account\n1. Check Balance\n2. Update Profile';
} else if (level === 1 && inputs[0] === '2') {
response = 'CON Enter phone number to pay:';
} else if (level === 2 && inputs[0] === '1' && inputs[1] === '1') {
// Check balance logic
response = `END Your balance is KES ${getBalance(phoneNumber)}`;
} else if (level === 2 && inputs[0] === '2') {
const recipientPhone = inputs[1];
response = `CON Pay to ${recipientPhone}\nEnter amount:`;
} else if (level === 3 && inputs[0] === '2') {
const recipientPhone = inputs[1];
const amount = inputs[2];
response = `CON Confirm payment of KES ${amount} to ${recipientPhone}?\n1. Confirm\n2. Cancel`;
}
// ... and so on
res.set('Content-Type', 'text/plain');
res.send(response);
});
When to use external session storage. The text string works for menu navigation, but sometimes you need to store state that does not fit in the menu path. For example, if you look up the user's account from your database during the session, you do not want to repeat that database query on every interaction. Use Redis or an in-memory store keyed by sessionId to cache session-specific data. Set the TTL (time to live) on the cache entry to match the USSD session timeout (usually 2 to 3 minutes). This way, stale sessions get cleaned up automatically.
Handling session timeouts. USSD sessions have a strict timeout, usually 30 to 180 seconds of inactivity depending on the carrier. If the user does not respond within this window, the session closes. Your server does not receive a "session ended" notification. The session just stops. Design your flows so that important operations (like confirming a payment) happen in as few steps as possible. Do not make users navigate through five menus to complete a single task.
If your application needs to perform long-running operations (checking an external API, processing a payment), do not make the user wait. Respond immediately with END Your request is being processed. You will receive an SMS confirmation. and handle the processing asynchronously. Send the result via SMS when it completes. Users are accustomed to this pattern from M-Pesa itself.
A USSD menu by itself is just text on a screen. The value comes from connecting it to your actual systems: databases, payment APIs, notification services, and business logic. Here is how to wire everything together.
Database queries. When a user selects "Check Balance," your server needs to look up their balance from your database. Use the phoneNumber parameter from the USSD request as the lookup key. Normalize the phone number format first (Africa's Talking sends it in +254 format, but your database might store 0712 or 254712). Build a normalization function and use it everywhere.
function normalizePhone(phone) {
// Remove + prefix and any spaces
let cleaned = phone.replace(/[+\s]/g, '');
// Convert 07xx to 2547xx
if (cleaned.startsWith('0')) {
cleaned = '254' + cleaned.substring(1);
}
return cleaned;
}
Payment integration. Triggering an M-Pesa STK Push from within a USSD session is a common pattern. The user selects "Pay," enters the amount, confirms, and you initiate an STK Push to their phone. Since the STK Push is asynchronous (the result comes via callback), respond to the USSD session immediately with END You will receive an M-Pesa prompt shortly. and handle the payment confirmation separately. Do not try to keep the USSD session alive while waiting for the M-Pesa callback. The session will time out.
SMS notifications. USSD is great for input but limited for output. If you need to send the user a receipt, a reference number, or detailed information, end the USSD session and send an SMS. Africa's Talking provides both USSD and SMS APIs, so you can trigger the SMS from the same backend. This pattern (USSD for interaction, SMS for confirmation) is what M-Pesa uses and what Kenyan users expect.
Authentication. USSD inherently identifies the user by their phone number, which acts as a basic form of authentication. For sensitive operations (financial transactions, account changes), add a PIN verification step. Store PINs as hashed values in your database, never in plain text. When the user enters their PIN via USSD, hash the input and compare it to the stored hash. This is the same pattern M-Pesa uses for its own PIN verification.
Error handling. If your database query fails or an external API is down, do not show the user a stack trace or a generic "Something went wrong." Show a human-readable message: END Service temporarily unavailable. Please try again in a few minutes. Log the actual error on your server. Users on feature phones cannot copy error messages or take screenshots. Keep error messages actionable and simple.
Moving from sandbox to production involves getting a real USSD shortcode, deploying your server to a reliable host, and testing thoroughly.
Getting a production shortcode. Contact Africa's Talking to request a production USSD shortcode. You will need to provide your business registration documents, a description of your service, and the shortcode you want (if a specific one is available). Shared shortcodes (where your service runs on a code like *384*xxx#) are cheaper and faster to get. Dedicated shortcodes (like *123#) are more expensive and take longer to provision because they require direct agreements with each telecom carrier. For most applications, a shared shortcode is fine to start with.
Server requirements. Your USSD server must be fast and reliable. USSD sessions time out if your server takes too long to respond, so latency matters. Use a server hosted in or near Kenya for the lowest latency. Cloud providers with Nairobi or Johannesburg regions (like AWS, Google Cloud, or Hetzner) work well. Your server must handle concurrent sessions. If 100 users dial your code simultaneously, your server receives 100 concurrent requests. Make sure your web framework and database connection pool can handle this.
SSL is required. Africa's Talking requires your callback URL to use HTTPS. Get an SSL certificate (Let's Encrypt is free) and configure it before setting your production callback URL.
Testing checklist:
Monitoring in production. Track session counts (how many sessions per hour), completion rates (how many users reach the final step vs how many abandon mid-flow), error rates, and average response times. Africa's Talking provides some analytics in their dashboard. For deeper insights, log every session interaction with timestamps and build your own analytics. The drop-off points in your menu tree tell you exactly where your UX is failing.
If building USSD apps, M-Pesa integrations, and the broader African developer stack interests you, our Full-Stack Software and AI Engineering course (KES 120,000) includes a complete module on building for the Kenyan market. You ship real projects with real payment integrations, not just sandbox exercises.
KES 9,999
Master Daraja API, STK Push, C2B, B2C, and callback handling. Build a real payment flow from scratch. 10 modules, 57 lessons.
Enrol in M-Pesa Integration