How to Create a USSD Code: Developer Guide for Africa

How to Create a USSD Code: What Is USSD and How Does It Work?
USSD (Unstructured Supplementary Service Data) is a real-time, session-based communication protocol built into every GSM network. Dial *920*100# on any phone and you’re using it.
Unlike SMS, USSD creates a live session between the user’s handset and your application server. That session stays open for the entire interaction. No internet is required, no app download is needed, and it works on any mobile phone, feature phones included.
A significant share of mobile users across Africa still rely on feature phones. USSD reaches all of them, which makes USSD application development a strategic investment for businesses in Ghana and the backbone of interactive customer experiences across Africa — from mobile money to self-service portals.
The protocol follows a straightforward request-response model:
- User dials a USSD code (e.g.,
*928*99#) - The mobile network routes the request to a USSD gateway
- The gateway forwards it to your application server as an HTTP POST
- Your server processes the request and returns a menu or response
- The user responds, and the cycle continues until the session ends
A session does not stay open forever. Under the 3GPP USSD specification, the network can release a session if a timer expires before a response arrives. The specification does not fix how long that timer runs, so the limit you work within is set by the network and your gateway.
Within that window, you can build multi-step flows — account lookups, payments, surveys, registration forms — all through numbered text menus.
USSD Gateway Architecture: How the Pieces Connect

Before writing your first line of code, understand the three layers that make a webhook-based USSD application work.
Layer 1: The Telco Network
Mobile network operators (MTN Ghana, Telecel Ghana, AirtelTigo) own the USSD infrastructure. When a subscriber dials your shortcode, their network’s USSD gateway handles the signaling. You don’t interact with this layer directly.
Layer 2: The USSD Gateway (Aggregator)
USSD gateway providers like Arkesel sit between the telco and your application. They handle shortcode provisioning, telco integrations, and session routing across multiple carriers.
When a user dials your code, the USSD gateway translates the telco’s signaling protocol into a clean HTTP POST to your server. With the right gateway provider, you avoid negotiating with each network and handling telco signalling yourself.
Layer 3: Your Application Server
Your server receives HTTP POST requests from the USSD API, processes input, manages session state, and returns a JSON response carrying the next screen. You control the menu logic, data access, and business rules. This is where you build.
The architecture flow:
User's Phone → Telco Network → USSD Gateway (Arkesel) → Your Server
↑ |
└────── JSON response (message + continueSession) ─────────┘Choosing a USSD Provider in Ghana
Selecting the right USSD gateway provider determines how fast you go live and how reliably your application performs across carriers. With several USSD providers in Ghana offering different capabilities, here’s what to evaluate.
Shared vs Dedicated Shortcodes
A shared shortcode (e.g., *928*99#) lets multiple businesses share one USSD code with unique extensions. Lower cost, faster setup — ideal for development and early-stage products.
A dedicated shortcode (e.g., *920#) is exclusively yours. Premium branding, and the code you request for production once testing on a shared code is done.
For a deeper walkthrough on shortcode options, see our guide on how to get a USSD shortcode in Ghana.
Multi-Network Coverage
Ghana has three major mobile networks: MTN Ghana, Telecel Ghana, and AirtelTigo. Your USSD gateway provider must route sessions across all three — or you lose reach to a significant slice of your user base.
Arkesel maintains direct connections to all three Ghanaian carriers, so a single USSD API integration covers the entire market.
API-First vs No-Code Platforms
If you’re a developer, you want an API-first USSD gateway — webhook-based session routing, JSON payloads, and full control over your menu logic.
If your team doesn’t have developers, ask any provider you are evaluating whether it offers a no-code menu builder, and see exactly what it covers before you commit. Arkesel also offers a managed USSD setup and builds the menu for teams without developers. For the API route, start with the USSD API documentation.
What Else to Look For
- Session routing architecture — webhook-based callbacks let you host logic on your own infrastructure
- Transparent pricing — check current USSD pricing before committing
- Developer documentation — clear API docs and code examples accelerate integration
- Uptime and reliability — for financial services and critical flows, uptime guarantees matter
Ready to build? Create your Arkesel developer account and request your USSD code.
Step-by-Step: How to Build a USSD Application with the Arkesel USSD API
Step 1: Set Up Your Arkesel Developer Account
- Create an account at account.arkesel.com/signup
- Request a USSD code for your account
- Provide your endpoint URL during the subscription process — Arkesel routes every USSD request for your code to this URL as an HTTP POST with a JSON body, and expects a JSON response back
Step 2: Configure Your USSD Shortcode
You have two options for getting a USSD shortcode:
- Shared shortcode: A code shared with other businesses (e.g.,
*928*99#). Lower cost, faster setup. - Dedicated shortcode: Your own exclusive code (e.g.,
*920#). Premium branding, requested when you are ready for production.
For development and testing, start with a shared shortcode. You can upgrade to a dedicated code once your application is live and gaining traction. Plan for that step: a dedicated USSD shortcode in Ghana requires assignment by the NCA (National Communications Authority), while a shared code is an extension of a code that is already assigned.
Step 3: Understand the USSD API Request
When a user interacts with your USSD code, Arkesel’s USSD API sends a POST request with a JSON body to your endpoint URL. The Arkesel developer documentation shows this sample request:
{
"sessionID": "2005506191900168",
"userID": "USSD_DOCUMENTATION",
"newSession": true,
"msisdn": "233271231234",
"userData": "*928*1#",
"network": "AIRTELTIGO"
}Key fields:
- sessionID — Identifies the session. Every request within the same session carries the same value, so use it to track state across interactions.
- userID — An identifier Arkesel creates for you during the subscription process.
- newSession —
truewhen the request starts a new session. - msisdn — The mobile number of the user making the request.
- userData — The value the user entered on their handset. In the documentation’s samples, it holds the dialled code on a new session and the user’s latest reply after that.
- network — The mobile network the request came from.
Note: Always reference the Arkesel developer documentation for the latest request format and field definitions.
Step 4: Build Your Response Logic
Your server must return a JSON response, and the documentation asks for all five of these fields:
{
"sessionID": "2005506191900168",
"userID": "USSD_DOCUMENTATION",
"msisdn": "233271231234",
"message": "Welcome to ARKESEL USSD Documentation",
"continueSession": false
}- sessionID, userID, msisdn — Echo back the values from the request.
- message — The text shown on the user’s screen.
- continueSession —
trueshows the message and waits for the user’s next input;falseshows the message and closes the session.
Because userData carries only the latest reply, your server keeps track of where each user is in the menu, keyed by sessionID.
Let’s build a practical example: a customer service menu for a mobile money application.
Code Example: Python (Flask)
This is how to create a USSD code application using Python and Flask. The example builds a QuickPay customer service menu with balance checks, money transfers, and airtime purchases.
from flask import Flask, request, jsonify
app = Flask(__name__)
sessions = {} # sessionID -> list of the user's replies so far
MAIN_MENU = "Welcome to QuickPay\n1. Check Balance\n2. Send Money\n3. Buy Airtime"
def menu(inputs, msisdn):
"""Return (message, continue_session) for the replies so far."""
level = len(inputs)
if level == 0:
return MAIN_MENU, True
if inputs[0] == '1':
return "Your balance is GHS 150.00", False
if inputs[0] == '2' and level == 1:
return "Enter recipient number:", True
if inputs[0] == '2' and level == 2:
return "Enter amount (GHS):", True
if inputs[0] == '2' and level == 3:
return f"Sending GHS {inputs[2]} to {inputs[1]}. You will receive an SMS confirmation.", False
if inputs[0] == '3' and level == 1:
return "Enter amount (GHS):", True
if inputs[0] == '3' and level == 2:
return f"Airtime of GHS {inputs[1]} purchased for {msisdn}", False
return "Invalid option. Please try again.", False
@app.route('/ussd', methods=['POST'])
def ussd_handler():
ussd_request = request.json
session_id = ussd_request.get('sessionID')
if ussd_request.get('newSession'):
inputs = []
else:
inputs = sessions.get(session_id, []) + [ussd_request.get('userData', '').strip()]
message, continue_session = menu(inputs, ussd_request.get('msisdn'))
if continue_session:
sessions[session_id] = inputs
else:
sessions.pop(session_id, None)
return jsonify({
"sessionID": session_id,
"userID": ussd_request.get('userID'),
"msisdn": ussd_request.get('msisdn'),
"message": message,
"continueSession": continue_session,
})
if __name__ == '__main__':
app.run(port=5000, debug=True)This example keeps each session’s replies in memory for simplicity. For production, replace with Redis (covered in the session management section below).
Code Example: Node.js (Express)
Here’s how to build a USSD application in Node.js — giving you the flexibility to develop in whichever language your team prefers.
const express = require('express');
const app = express();
app.use(express.json());
const sessions = new Map(); // sessionID -> array of the user's replies so far
function menu(inputs, msisdn) {
const level = inputs.length;
if (level === 0) {
return ['Welcome to QuickPay\n1. Check Balance\n2. Send Money\n3. Buy Airtime', true];
}
if (inputs[0] === '1') return ['Your balance is GHS 150.00', false];
if (inputs[0] === '2' && level === 1) return ['Enter recipient number:', true];
if (inputs[0] === '2' && level === 2) return ['Enter amount (GHS):', true];
if (inputs[0] === '2' && level === 3) {
return [`Sending GHS ${inputs[2]} to ${inputs[1]}. SMS confirmation will follow.`, false];
}
if (inputs[0] === '3' && level === 1) return ['Enter amount (GHS):', true];
if (inputs[0] === '3' && level === 2) {
return [`Airtime of GHS ${inputs[1]} purchased for ${msisdn}`, false];
}
return ['Invalid option. Please try again.', false];
}
app.post('/ussd', (req, res) => {
const { sessionID, userID, newSession, msisdn, userData } = req.body;
const inputs = newSession
? []
: [...(sessions.get(sessionID) || []), String(userData || '').trim()];
const [message, continueSession] = menu(inputs, msisdn);
if (continueSession) {
sessions.set(sessionID, inputs);
} else {
sessions.delete(sessionID);
}
res.json({ sessionID, userID, msisdn, message, continueSession });
});
app.listen(3000, () => console.log('USSD app running on port 3000'));Both examples follow identical logic. Pick the language your team knows best: USSD programming against an HTTP endpoint that exchanges JSON is language-agnostic. The developer documentation also includes request-handling samples in Java, Python and PHP.
USSD Session Management Best Practices

Plan for sessions that time out, drop, or arrive at the same time.
Handle Timeouts Gracefully
There is no single USSD timeout to design around. Networks and gateways set their own, and Arkesel’s USSD API tracks sessions automatically with configurable timeouts. Your application must account for this:
- Keep menu depths shallow — 3–4 levels maximum. Each level consumes session time.
- Store partial session data server-side so users can resume if a session drops.
- Send an SMS confirmation for completed transactions — users won’t always see the final USSD screen.
Choose the Right State Persistence Strategy (Redis vs Database)
You have three options for tracking session state:
| Strategy | Best For | Trade-off |
|---|---|---|
| In-memory (dictionary/Map) | Prototyping, single-server setups | Lost on restart, doesn’t scale |
| Redis | Production, multi-server deployments | Fast, auto-expiry with TTL, scales horizontally |
| Database | Complex flows needing audit trails | Slower reads, but persistent and queryable |
For production USSD applications, Redis is a good fit: it keeps session state outside any single server and can expire it automatically. Set your TTL from the session timeout configured on your gateway, read from configuration rather than hardcoded, so stale sessions clean themselves up and the value follows any change to the timeout.
import json
import os
import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# Session timeout in seconds, taken from your gateway configuration
SESSION_TTL = int(os.environ["USSD_SESSION_TTL"])
# Store session state with auto-expiry
r.setex(f"ussd:{session_id}", SESSION_TTL, json.dumps(session_data))
# Retrieve session state
data = json.loads(r.get(f"ussd:{session_id}") or '{}')Why Redis over a database for USSD? USSD sessions are short-lived, and every menu step needs the session’s state. An in-memory store like Redis avoids a database round-trip on every step.
Use a database alongside Redis when you need persistent audit trails for financial transactions or compliance.
Parse Input Correctly
The userData field carries the value the user just entered, not their whole journey. A user navigating Main Menu → Send Money → Enter Number sends 2 in one request and 0241234567 in the next, both with the same sessionID.
Append each reply to the session’s stored list and use the list length to determine menu depth. Check newSession first: on a new session, userData holds the dialled code rather than a menu choice. Trim whitespace before you compare values.
Handle Concurrent Sessions
When multiple users hit your shortcode simultaneously, race conditions surface if your code isn’t thread-safe. Use the sessionID as the unique key for all state operations. Avoid global mutable state. If you’re using Redis, each session ID naturally isolates data.
Test with concurrent load using tools like k6 or Apache Bench to verify your application handles parallel sessions without state leaks.
Securing Your USSD Application
USSD applications often handle sensitive data — financial transactions, personal details, authentication flows. Security cannot be an afterthought. For a comprehensive treatment, see our guide on USSD security best practices.
Validate and Sanitize All Input
Users can type anything into a USSD prompt — not just the numbers you expect. Validate every input against expected patterns before processing:
import re
def validate_phone(number):
return bool(re.match(r'^0[235]\d{8}$', number))
def validate_amount(amount):
try:
val = float(amount)
return 0 < val <= 10000
except ValueError:
return False
def sanitize_input(text):
return text.strip()[:20] # Limit length, strip whitespaceNever pass raw USSD input directly to database queries or shell commands. Treat every input as untrusted.
Protect Against Session Hijacking
- Validate session origin — if your gateway publishes the source IP ranges its requests come from, whitelist them in your firewall or application middleware. Otherwise, ask your provider how to confirm a request is genuine.
- Bind sessions to phone numbers — if a
sessionIDarrives with a differentmsisdnthan the one that started it, reject the request. - Set strict TTLs — expire session data in Redis once your gateway’s session timeout passes. Lingering sessions are attack surface.
Secure Sensitive Data in Transit
- HTTPS only — your endpoint URL must use TLS. No exceptions.
- Never log sensitive data — PINs, account numbers, and OTPs must not appear in application logs or error messages.
- Rate limiting — cap the number of USSD sessions per phone number per minute to prevent brute-force attacks on PIN-protected flows.
Testing Your USSD Application
Thorough testing is critical before going live on Ghanaian networks. Once you know how to create a USSD code and build your USSD application, the next step is validating every menu path.
Local Development Testing
Start by testing your endpoint locally. Use curl or Postman to send a request in the documented JSON format:
curl -X POST http://localhost:5000/ussd \
-H "Content-Type: application/json" \
-d '{"sessionID": "test-001", "userID": "USSD_DOCUMENTATION", "newSession": true, "msisdn": "233271231234", "userData": "*928*1#", "network": "AIRTELTIGO"}'Send a second request with the same sessionID, "newSession": false and "userData": "1" to walk one step into the menu.
To expose your local server to Arkesel’s requests during development, use a tunneling tool like ngrok:
ngrok http 5000Use that HTTPS URL as your endpoint URL while you test, so you can exercise the full request flow without deploying to a server.
Staging Environment Testing
Once your logic passes locally, test on real handsets. Arkesel lets you start on a shared USSD short code for testing and move to a dedicated code for production, so you catch integration issues (endpoint URL accessibility, request parsing, response formatting) before your own code goes live.
If your flow sends SMS confirmations, the SMS API sandbox lets you exercise those calls without spending live credit.
Automated Testing
Write unit tests for your menu logic. Because menu() is a pure function — given the replies so far, it returns the message and whether the session continues — USSD apps are highly testable.
def test_main_menu():
message, continue_session = menu([], '233271231234')
assert continue_session is True
assert 'Check Balance' in message
def test_check_balance():
message, continue_session = menu(['1'], '233271231234')
assert continue_session is False
assert 'GHS' in message
def test_invalid_input():
message, continue_session = menu(['9'], '233271231234')
assert continue_session is False
assert 'Invalid' in messageFor end-to-end testing of multi-step flows, build a script that replays a sequence of JSON requests with one sessionID and validates each response. This catches state management bugs that unit tests miss.
Deploying on Ghana’s Telco Networks

Taking your USSD application from development to production on Ghanaian carriers requires testing how each network handles your menus.
Carrier-Specific Considerations
Ghana’s three mobile networks each have nuances:
- MTN Ghana — Run your full menu and timeout tests here before launch, as you will on the other two networks.
- Telecel Ghana — Run the same menu and timeout tests here before launch.
- AirtelTigo — Essential for full market coverage. Test timeout handling on every network, this one included.
Go Live Checklist
- ☑ Endpoint URL is HTTPS, publicly accessible, and responds promptly
- ☑ Response logic tested on all three carriers (MTN, Telecel, AirtelTigo)
- ☑ Session state persisted in Redis (not in-memory)
- ☑ Error handling returns a user-friendly
messagewithcontinueSessionset tofalse(never raw stack traces) - ☑ Monitoring in place for session completion rates and response times
- ☑ SMS fallback configured for interrupted critical transactions
- ☑ Menu design tested for character limits (the USSD character-set standard allows up to 182 characters of GSM 7-bit text per message, so keep every screen well under it)
- ☑ Input validation and sanitization active on all user-facing prompts
Common Pitfalls and Debugging Tips
These issues commonly trip up developers building USSD applications.
1. Response Too Long
A USSD message in the GSM 7-bit alphabet holds up to 182 characters, and fewer if your text needs another encoding. A menu written past that ceiling will not reach the user as you wrote it.
Fix: Keep each response well under 182 characters. Use abbreviations and short labels. Test on actual handsets, not just simulators.
2. Endpoint URL Not Reachable
Your server must be publicly accessible, respond promptly, and return a 200 status code.
Fix: Verify your URL is HTTPS-enabled and accessible from outside your network. Use health check endpoints. Monitor response times.
3. Session State Lost Between Requests
If your server restarts or you’re running multiple instances behind a load balancer, in-memory session stores vanish.
Fix: Use Redis or a database for session persistence. Never rely on server memory in production.
4. Character Encoding Issues
Special characters and non-ASCII text (common in local languages) can corrupt USSD responses.
Fix: Stick to the GSM 7-bit character set, which also gives you the most characters per screen. Avoid emojis, special symbols, and characters outside the standard GSM alphabet.
5. Not Handling Concurrent Sessions
Multiple users hitting your shortcode simultaneously creates race conditions if your code isn’t thread-safe.
Fix: Use session ID as the unique key for all state operations. Avoid global variables. Test with concurrent load using tools like Apache Bench or k6.
6. Ignoring Network Variability
Different carriers handle USSD slightly differently. A flow that works on MTN Ghana might behave differently on Telecel.
Fix: Test across all target carriers before launch. Log the network field from each request to identify carrier-specific issues.
7. Not Escaping Special Characters in USSD Codes
USSD codes contain * and #, which have special meaning in many programming contexts (regex, Markdown, shell). Improper escaping corrupts display or breaks parsing.
Fix: Wrap USSD codes in code blocks or escape * and # in your rendering layer. In documentation and user-facing output, validate that codes like *928*99# display correctly.
Scaling Your USSD Application
Once your application handles real traffic, performance becomes critical. USSD users expect instant responses — any delay feels like the session is hanging.
- Response time: Measure it on every request and alert when it climbs. Every slow response eats into a session timer you do not control.
- Horizontal scaling: Run multiple server instances behind a load balancer. Use Redis for shared session state so any instance can handle any request.
- Database optimization: Cache frequently accessed data (account balances, user profiles). Keep slow database queries out of the request path.
- Monitoring: Track session completion rates, average response times, and error rates per carrier. Drop-offs at specific menu levels reveal UX problems.
- Load testing: Simulate concurrent USSD sessions at peak volumes. Load-test well above your normal peak, so a busy day does not become the first real test of your capacity.
For high-throughput USSD services (mobile money, airtime top-up, payment processing), consider a dedicated load balancer layer with health checks that remove unresponsive instances from the pool automatically.
What to Build Next
You now know how to create a USSD code and have a working application with proper session management, tested and ready for deployment. Here’s where to go from here:
- Add SMS confirmations: Send transaction receipts via Arkesel’s SMS Platform after USSD interactions complete.
- Optimize your menus: Apply the 10 best practices for USSD menu design to increase completion rates.
- Build financial services: Explore USSD for mobile money and financial services to add payment capabilities.
- Compare channels: Evaluate whether USSD or a mobile app is the right primary channel for your users, or explore USSD vs SMS vs WhatsApp for your communication mix.
- Secure your application: Implement the full USSD security framework for production transaction flows.
- Explore healthcare: See how USSD powers patient services across Africa without requiring an app.
Ready to build your USSD application? Create your Arkesel developer account, request your USSD code, and follow the USSD API documentation to go live.
Frequently Asked Questions
How long does it take to set up a USSD application?
Writing the handler for a simple menu is a matter of hours, as the code examples above show. Going live takes longer: start testing on a shared short code, which is quicker to start on than a dedicated one, then request a dedicated code when you are ready for production.
Can I build a USSD application without a shortcode?
You need a shortcode to reach users on live networks. Before that, you can test your menu logic locally with unit tests and a script that replays a sequence of JSON requests against your endpoint. When you are ready for real handsets, begin on a shared short code for testing and move to a dedicated code for production, so you validate your menu flows, session management and business logic before committing to your own code.
What programming languages work with the USSD API?
Any language that handles HTTP POST requests works. When you learn how to create a USSD code, the language choice rarely matters: the USSD API sends your endpoint an HTTP POST with a JSON body, and your server returns a JSON response. Arkesel’s developer documentation includes request-handling samples in Java, Python and PHP.
How do I handle USSD sessions that timeout?
There is no fixed USSD session length, because networks and gateways set their own timeouts. Store session state in Redis with a TTL read from your gateway’s configured session timeout, not a hardcoded number. When a session expires, clean up any pending transactions.
For financial applications, implement idempotency keys so retried transactions don’t process twice. Send an SMS to the user if their session was interrupted during a critical flow.
What is the character limit for USSD messages?
A USSD message written in the standard GSM 7-bit alphabet can carry up to 182 characters. Text that needs another encoding, such as some non-Latin characters, fits fewer. Keep your menus well under the ceiling and test them on real handsets on every network. Use numbered options (1, 2, 3) instead of lettered ones to save characters.
How much does a USSD shortcode cost in Ghana?
Pricing depends on whether you choose a shared or dedicated shortcode, your expected session volume, and your provider. Shared shortcodes cost less and launch faster.
Dedicated codes carry a premium. Check Arkesel’s current pricing for the latest rates.
What is the difference between shared and dedicated USSD shortcodes?
A shared shortcode (e.g., *928*99#) is one code used by multiple businesses, each with a unique extension. It’s faster to set up and more cost-effective. A dedicated shortcode (e.g., *920#) belongs exclusively to your business — stronger brand recognition, and the code you request for production after testing on a shared one.





