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 required, no app download needed, works on every phone ever made.
Here’s why that matters for Africa: a significant share of mobile users across the continent still rely on feature phones. USSD reaches all of them.
The protocol follows a straightforward request-response model:
- User dials a USSD code (e.g., *384*1234#)
- The mobile network routes the request to an aggregator
- The aggregator forwards it to your application server via HTTP
- Your server processes the request and returns a menu or response
- The user responds, and the cycle continues until the session ends
Each session typically lasts up to 180 seconds. 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 USSD application development work.
Layer 1: The Telco Network
Mobile network operators (MTN, Vodafone, AirtelTigo in Ghana) 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 Aggregator
Aggregators like Arkesel sit between the telco and your application. They handle shortcode provisioning, telco integrations, and session routing. When a user dials your code, the aggregator translates the telco’s signaling protocol into a clean HTTP POST to your server.
This is where your USSD for business journey starts — with the right aggregator, you skip months of carrier negotiations and complex protocol work.
Layer 3: Your Application Server
Your server receives HTTP POST requests, processes input, manages session state, and returns text responses. You control the menu logic, data access, and business rules. This is where you build.
The architecture flow looks like this:

Text version of the architecture diagram
User's Phone → Telco Network → Aggregator (Arkesel) → Your Server
↑ |
└──────────── Response (text menu) ────────────────────┘
Step-by-Step: How to Create a USSD Code with the Arkesel API
Let’s build a working USSD application from scratch. You’ll set up your developer account, configure a shortcode, and write the code that powers your menus.
Step 1: Set Up Your Arkesel Developer Account
- Create an account at account.arkesel.com/signup
- Navigate to the USSD section in your dashboard
- Generate your API key (you’ll need this for authentication)
- Set your callback URL — this is the endpoint Arkesel will POST to when users interact with your USSD code
Your callback URL must be publicly accessible. This USSD API integration requires a public endpoint — during development, use a tunneling tool like ngrok to expose your local server.
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., *384*100#). Lower cost, faster setup.
- Dedicated shortcode: Your own exclusive code (e.g., *384*1234#). Premium branding, requires NCA registration in Ghana.
For development and testing, start with a shared shortcode. You can upgrade to a dedicated code once your application is live and gaining traction.
Step 3: Understand the Callback Payload
When a user interacts with your USSD code, Arkesel sends a POST request to your callback URL with these parameters:
{
"sessionId": "a-unique-session-identifier",
"serviceCode": "*384*1234#",
"phoneNumber": "+233241234567",
"text": "",
"type": "initiation"
}
Key fields:
- sessionId — Unique identifier for this USSD session. Use it to track state across multiple interactions.
- serviceCode — The USSD code the user dialed.
- phoneNumber — The subscriber’s number (MSISDN format).
- text — The user’s cumulative input. Empty on first request. Subsequent inputs are appended with
*as separator (e.g., “1*2*John”). - type — Either “initiation” (first request) or “response” (subsequent interaction).
Step 4: Build Your Response Logic
Your server must return a plain text response. Two response types:
- Continue session: Prefix with
CON— shows a menu and waits for input - End session: Prefix with
END— displays a message and closes the session
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
app = Flask(__name__)
sessions = {}
@app.route('/ussd', methods=['POST'])
def ussd_callback():
session_id = request.form.get('sessionId')
phone_number = request.form.get('phoneNumber')
text = request.form.get('text', '')
inputs = text.split('*') if text else []
level = len(inputs)
if level == 0:
response = "CON Welcome to QuickPay\n"
response += "1. Check Balance\n"
response += "2. Send Money\n"
response += "3. Buy Airtime\n"
response += "4. My Account"
elif level == 1 and inputs[0] == '1':
balance = get_balance(phone_number)
response = f"END Your balance is GHS {balance:.2f}"
elif level == 1 and inputs[0] == '2':
response = "CON Enter recipient phone number:"
elif level == 2 and inputs[0] == '2':
recipient = inputs[1]
sessions[session_id] = {'recipient': recipient}
response = "CON Enter amount (GHS):"
elif level == 3 and inputs[0] == '2':
amount = inputs[2]
recipient = sessions.get(session_id, {}).get('recipient', inputs[1])
response = f"CON Send GHS {amount} to {recipient}?\n"
response += "1. Confirm\n"
response += "2. Cancel"
elif level == 4 and inputs[0] == '2':
if inputs[3] == '1':
response = "END Transaction submitted. You will receive a confirmation SMS."
else:
response = "END Transaction cancelled."
elif level == 1 and inputs[0] == '3':
response = "CON Enter amount (GHS):"
elif level == 2 and inputs[0] == '3':
amount = inputs[1]
response = f"END Airtime of GHS {amount} purchased for {phone_number}."
elif level == 1 and inputs[0] == '4':
response = "CON My Account\n"
response += "1. Change PIN\n"
response += "2. Mini Statement\n"
response += "3. Back to Main Menu"
else:
response = "END Invalid input. Please try again."
return response
def get_balance(phone_number):
return 150.75
if __name__ == '__main__':
app.run(port=5000, debug=True)
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.urlencoded({ extended: true }));
app.use(express.json());
const sessions = new Map();
app.post('/ussd', (req, res) => {
const { sessionId, phoneNumber, text } = req.body;
const inputs = text ? text.split('*') : [];
const level = inputs.length;
let response = '';
if (level === 0) {
response = 'CON Welcome to QuickPay\n';
response += '1. Check Balance\n';
response += '2. Send Money\n';
response += '3. Buy Airtime\n';
response += '4. My Account';
} else if (level === 1 && inputs[0] === '1') {
const balance = getBalance(phoneNumber);
response = `END Your balance is GHS ${balance.toFixed(2)}`;
} else if (level === 1 && inputs[0] === '2') {
response = 'CON Enter recipient phone number:';
} else if (level === 2 && inputs[0] === '2') {
sessions.set(sessionId, { recipient: inputs[1] });
response = 'CON Enter amount (GHS):';
} else if (level === 3 && inputs[0] === '2') {
const session = sessions.get(sessionId) || {};
const amount = inputs[2];
response = `CON Send GHS ${amount} to ${session.recipient || inputs[1]}?\n`;
response += '1. Confirm\n';
response += '2. Cancel';
} else if (level === 4 && inputs[0] === '2') {
sessions.delete(sessionId);
if (inputs[3] === '1') {
response = 'END Transaction submitted. You will receive a confirmation SMS.';
} else {
response = 'END Transaction cancelled.';
}
} else if (level === 1 && inputs[0] === '3') {
response = 'CON Enter amount (GHS):';
} else if (level === 2 && inputs[0] === '3') {
response = `END Airtime of GHS ${inputs[1]} purchased for ${phoneNumber}.`;
} else if (level === 1 && inputs[0] === '4') {
response = 'CON My Account\n';
response += '1. Change PIN\n';
response += '2. Mini Statement\n';
response += '3. Back to Main Menu';
} else {
response = 'END Invalid input. Please try again.';
}
res.set('Content-Type', 'text/plain');
res.send(response);
});
function getBalance(phoneNumber) {
return 150.75;
}
app.listen(5000, () => {
console.log('USSD server running on port 5000');
});
Session Management Best Practices
Session handling separates solid USSD apps from frustrating ones. Every developer building for African networks needs to get this right.
Handle Timeouts Gracefully
USSD sessions timeout after 120-180 seconds depending on the carrier. 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
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 applications, Redis is the standard choice. Set your TTL to match the USSD session timeout (180 seconds) so stale sessions clean themselves up.
import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# Store session data with 180-second TTL
r.setex(f"ussd:{session_id}", 180, json.dumps({
'level': 2,
'recipient': '+233241234567',
'phone': phone_number
}))
Parse Input Correctly
The text field accumulates all user inputs separated by *. A user navigating Main Menu -> Send Money -> Enter Number produces: "2*0241234567".
Always split on * and use the array length to determine menu depth. Never rely on the raw text string directly — edge cases with asterisks in input will break your flow.
Testing Your USSD Application
Thorough testing is critical before going live on African networks. Once you know how to create a USSD code, the next step is validating every menu path. A buggy USSD app doesn’t just frustrate users — it costs them airtime.
Local Development Testing
- Expose your local server: Use ngrok to create a public URL pointing to your localhost.
ngrok http 5000
Copy the HTTPS URL and set it as your callback in the Arkesel dashboard.
- Simulate USSD requests: Use curl or Postman to send POST requests matching the callback payload format.
curl -X POST http://localhost:5000/ussd \
-d "sessionId=test-001&serviceCode=*384*1234#&phoneNumber=+233241234567&text=&type=initiation"
- Test every menu path: Map out all possible user journeys and test each one. Pay special attention to invalid inputs and edge cases.
Staging Environment Testing
Arkesel’s sandbox environment lets you test with simulated USSD sessions before connecting to live telco networks. This catches integration issues — callback URL accessibility, payload parsing, response formatting — without burning real shortcode sessions.
Automated Testing
Write unit tests for your menu logic. Each menu state is a pure function: given a text input, it returns a response string. This makes USSD apps highly testable.
def test_main_menu():
response = ussd_callback(session_id='test', text='')
assert response.startswith('CON')
assert 'Check Balance' in response
def test_check_balance():
response = ussd_callback(session_id='test', text='1')
assert response.startswith('END')
assert 'GHS' in response
def test_invalid_input():
response = ussd_callback(session_id='test', text='9')
assert response.startswith('END')
assert 'Invalid' in response
Deploying on African Telco Networks
Taking your USSD application from development to production across African carriers requires understanding the regulatory and technical landscape.
Carrier-Specific Considerations
Ghana (MTN, Vodafone, AirtelTigo):
- All USSD shortcodes require NCA (National Communications Authority) registration for dedicated codes
- Shared shortcodes through aggregators like Arkesel bypass individual carrier negotiations
- MTN has the largest subscriber base — test here first
Nigeria (MTN, Airtel, Glo, 9mobile):
- NCC regulates shortcode allocation
- High USSD traffic volume — design for concurrent sessions
Kenya (Safaricom, Airtel):
- Mature USSD market driven by M-Pesa
- Safaricom’s USSD sessions allow slightly longer timeouts
Go Live Checklist
- Deploy your application to a production server with SSL (HTTPS callbacks required)
- Set up Redis or equivalent for session management
- Configure your production callback URL in the Arkesel dashboard
- Test every menu path on the live shortcode with real handsets
- Set up monitoring and alerting for failed sessions
- Implement logging for every USSD interaction (compliance requirement in most African markets)
Common Pitfalls and Debugging Tips
After working with hundreds of USSD deployments across Africa, these are the issues that trip up developers most often.
1. Response Too Long
USSD screens display a maximum of 182 characters (160 on some networks). Exceed this and your menu gets truncated — or worse, the session fails silently.
Fix: Keep each response under 160 characters. Use abbreviations and short labels. Test on actual handsets, not just simulators.
2. Callback URL Not Reachable
The most common integration failure. Your server must be publicly accessible, respond within 10 seconds, 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 GSM 7-bit character set. 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 break on Vodafone.
Fix: Test across all target carriers before launch. Log the serviceCode and carrier metadata to identify carrier-specific issues.
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 target: Under 2 seconds. Anything over 5 seconds risks session timeout on some networks.
- Horizontal scaling: Run multiple server instances behind a load balancer. Use Redis for shared session state.
- Database optimization: Cache frequently accessed data (account balances, user profiles). Avoid database queries that take more than 500ms.
- Monitoring: Track session completion rates, average response times, and error rates per carrier. Use Kova IQ for real-time analytics across your communication channels.
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 API 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.
Ready to build? Create your Arkesel developer account and deploy your first USSD application today.
Frequently Asked Questions
How long does it take to set up a USSD application?
With Arkesel’s API, you can learn how to create a USSD code and build a working application in a single day. The code itself takes hours. Getting a live shortcode connected to production telco networks typically takes 1-3 business days for shared codes, or 2-4 weeks for dedicated shortcodes that require NCA registration.
Can I build a USSD application without a shortcode?
You need a shortcode to reach users on live networks. However, you can build and test your entire application logic using Arkesel’s sandbox environment before securing a shortcode. This lets you validate your menu flows, session management, and business logic before committing to a code.
What programming languages work with the USSD API?
Any language that handles HTTP POST requests works. The USSD API uses standard HTTP callbacks — your server receives a POST request and returns plain text. The approach is similar to Arkesel’s SMS API integration — if you’ve worked with REST APIs, you’ll be productive immediately. Python (Flask, Django), Node.js (Express), PHP, Java (Spring Boot), Ruby, and Go are all common choices among African developers.
How do I handle USSD sessions that timeout?
Store session state in Redis with a TTL matching the carrier timeout (typically 180 seconds). 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?
The standard limit is 182 characters per screen, though some carriers cap it at 160 characters. Design your menus to stay under 160 characters for maximum carrier compatibility. Use numbered options (1, 2, 3) instead of lettered ones to save characters.
Explore the USSD Business Series
This guide is part of a comprehensive series on building with USSD in Africa.
- USSD for Business in Africa: How to Build Interactive Customer Experiences (Pillar Guide)
- How to Get a USSD Shortcode for Your Business in Ghana
- USSD vs Mobile App in Africa: Which Channel Wins?
- USSD Financial Services in Africa: Mobile Money Guide
- USSD Menu Design: 10 Best Practices for Higher Completion Rates






