USSD application development illustration showing a code editor with Python Flask USSD callback code connected through an API gateway to a mobile phone displaying a USSD menu with numbered options

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 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:

  1. User dials a USSD code (e.g., *384*1234#)
  2. The mobile network routes the request to an aggregator
  3. The aggregator forwards it to your application server via HTTP
  4. Your server processes the request and returns a menu or response
  5. 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:

USSD gateway architecture diagram showing Developer App Server, Arkesel API Gateway, Telco Networks (MTN, Vodafone, AirtelTigo), and User Phone with USSD menu
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

  1. Create an account at account.arkesel.com/signup
  2. Navigate to the USSD section in your dashboard
  3. Generate your API key (you’ll need this for authentication)
  4. 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:

StrategyBest ForTrade-off
In-memory (dictionary/Map)Prototyping, single-server setupsLost on restart, doesn’t scale
RedisProduction, multi-server deploymentsFast, auto-expiry with TTL, scales horizontally
DatabaseComplex flows needing audit trailsSlower 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

  1. 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.

  1. 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"
  1. 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

  1. Deploy your application to a production server with SSL (HTTPS callbacks required)
  2. Set up Redis or equivalent for session management
  3. Configure your production callback URL in the Arkesel dashboard
  4. Test every menu path on the live shortcode with real handsets
  5. Set up monitoring and alerting for failed sessions
  6. 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:

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.

Popular Posts

Your OTP provider shapes every verification your users experience. The wrong choice means failed deliveries in African markets, inflated costs from SMS pumping fraud, and authentication flows that frustrate customers instead of protecting them. The

What Is SMS Pumping (Artificially Inflated Traffic)? SMS pumping — also called Artificially Inflated Traffic (AIT) or SMS toll fraud — is a scheme where attackers exploit your OTP and verification endpoints to generate massive

Your OTP expires before the user finishes typing. Or worse, it never expires at all, and an attacker walks right through your verification flow. OTP best practices — expiration windows, rate limits, input UX —

Scroll to Top