Building a USSD App in Kenya From Scratch
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.
How USSD Actually Works (And Why It Still Matters)
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:
- No internet required. USSD runs over the GSM signaling channel, not data. The user does not need an active data bundle, Wi-Fi, or a smartphone. They just need cell signal.
- Session-based. When a user dials your USSD code, a session opens between their phone and your server. Every time they respond, the session continues. When they stop responding or the timeout expires, the session closes. This is fundamentally different from HTTP where each request is independent.
- Text only. No images, no buttons, no colors. Your entire UI is plain text with numbered menu options. This constraint forces simplicity, which is often a good thing.
- Real-time. USSD sessions happen in real time. The user dials, your server responds, the user replies, your server responds again. There is no "loading" state. If your server takes more than a couple of seconds to respond, the session may time out.
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.
Setting Up With Africa's Talking
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.
Session Management and State Tracking
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.
Connecting USSD to Your Backend Systems
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.
Going to Production: Shortcodes, Testing, and Deployment
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:
- Test every path through your menu tree, including invalid inputs at every level
- Test with very long user inputs (what happens if someone types 50 characters when you expect a 4-digit PIN?)
- Test with special characters in user input (asterisks, hash signs, spaces)
- Test your response lengths. If a response exceeds the character limit, it gets truncated. Count characters carefully
- Test on real feature phones on Safaricom, Airtel, and Telkom. Behavior can differ slightly between carriers
- Test your server under load. Use a tool like k6 or Artillery to simulate 100 concurrent sessions and make sure response times stay under 2 seconds
- Test what happens when your database is slow or unavailable
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.
Key Takeaways
- ✓USSD works on every phone with a SIM card, no internet or smartphone required. In Kenya, this means you can reach users that apps and websites cannot.
- ✓USSD is session-based with a strict timeout (usually 30 to 180 seconds depending on the carrier). Your menus must be short and your server must respond fast.
- ✓Africa's Talking is the most developer-friendly USSD gateway in Kenya. They handle the telecom side so you can focus on building your application logic.
- ✓Menu design matters more than code quality. A confusing menu tree wastes sessions and frustrates users who are paying per session on some networks.
- ✓USSD is not a replacement for a mobile app. It is a complement. Use USSD for quick transactions and status checks, and push users to your app for complex interactions.
Frequently Asked Questions
- How much does it cost to get a USSD shortcode in Kenya?
- Through Africa's Talking, shared shortcodes are affordable (fees depend on your usage volume and are charged per session). Dedicated shortcodes are more expensive because they require agreements with each telecom carrier. The sandbox is completely free for testing. Contact Africa's Talking directly for current production pricing, as it varies based on your expected traffic and whether you want a shared or dedicated code.
- Can I build a USSD app with Python instead of Node.js?
- Yes. Africa's Talking's USSD API is language-agnostic. Your server receives POST requests and returns plain text. Flask, Django, or FastAPI all work perfectly. Africa's Talking also provides official SDKs for Python, Node.js, PHP, Java, Ruby, and several other languages. The session management and menu logic patterns are identical regardless of your language choice.
- Do users get charged for using my USSD service?
- This depends on the carrier and your shortcode agreement. Some USSD services are free for the end user (the business absorbs the cost per session). Others charge the user a small fee per session (typically a few shillings). Clarify this with Africa's Talking and the carrier during your go-live process. User-paid USSD can reduce adoption, especially for price-sensitive audiences. Many successful services absorb the cost and recover it through the transactions that happen within the app.
- What is the character limit for USSD messages?
- The standard USSD message length is 182 characters, though this can vary by carrier and phone model. Some older feature phones display fewer characters. Keep your menu screens well under this limit. If a response is too long, it gets truncated without warning, and the user sees a broken menu. Count characters during development and test on real devices to confirm everything displays correctly.
- Can I integrate M-Pesa payments directly into my USSD app?
- Yes, and this is one of the most common patterns. From within your USSD handler, trigger an M-Pesa STK Push to the user's phone number. End the USSD session immediately (do not wait for the M-Pesa callback). The user receives the M-Pesa payment prompt on their phone, enters their PIN, and you receive the payment confirmation via your M-Pesa callback URL. Send the user an SMS confirmation once the payment is processed.
- Is USSD still relevant with smartphone adoption growing in Kenya?
- Yes. Even as smartphone adoption grows, USSD remains relevant for several reasons. Data bundles are expensive and unreliable in many areas. Feature phones are still widely used in rural Kenya. USSD works during network congestion when data services struggle. And even smartphone users sometimes prefer USSD for quick transactions because it is faster than opening an app. M-Pesa, the most successful financial product in Kenya, still runs primarily on USSD. That tells you everything about its relevance.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon