Skip to main content

Overview

The USSD service enables you to build interactive menu-driven applications accessible via shortcodes (e.g., *123#). USSD sessions are stateful and support navigation through multiple menu levels.

Service Status

Check USSD Service Status

curl http://localhost:8000/ussd/
Response
Welcome to the USSD service

USSD Session Handling

Handle USSD Session

Process USSD requests and return menu responses.
curl -X POST http://localhost:8000/ussd/session \
  -d "sessionId=ATUid_123" \
  -d "serviceCode=*123#" \
  -d "phoneNumber=%2B254711000111" \
  -d "text="
sessionId
string
required
Unique session identifier from Africa’s Talking
serviceCode
string
required
USSD shortcode that was dialed (e.g., *123#)
phoneNumber
string
required
User’s phone number
text
string
required
User’s input (empty for first request, then contains menu selections)Examples:
  • "" - First request (main menu)
  • "1" - User selected option 1
  • "1*1" - User selected 1, then 1 (navigation path)
Implementation
# From routes/ussd.py:11-46
@ussd_bp.route("/session", methods=["POST"])
def ussd_handler():
    # Read the variables sent via POST from our API
    session_id = request.values.get("sessionId", None)
    serviceCode = request.values.get("serviceCode", None)
    phone_number = request.values.get("phoneNumber", None)
    text = request.values.get("text", "")

    # Print the details
    print(f"session_id: {session_id}")
    print(f"serviceCode: {serviceCode}")

    if text == "":
        # First request. Start response with CON
        response = "CON What would you want to check \n"
        response += "1. My Account \n"
        response += "2. My phone number"

    elif text == "1":
        # First level response
        response = "CON Choose account information you want to view \n"
        response += "1. Account number"

    elif text == "2":
        # Terminal request
        response = "END Your phone number is " + str(phone_number)

    elif text == "1*1":
        # Second level response
        accountNumber = "ACC1001"
        response = "END Your account number is " + accountNumber

    else:
        response = "END Invalid choice"

    return response

USSD Response Format

Response Prefixes

USSD responses must start with either CON or END:
Used when you want to show a menu and wait for user input.
response = "CON What would you want to check \n"
response += "1. My Account \n"
response += "2. My phone number"
Output on phone:
What would you want to check
1. My Account
2. My phone number
Used to display final message and terminate the session.
response = "END Your phone number is +254711000111"
Output on phone:
Your phone number is +254711000111
Session ends after this message.

The demo application implements this menu structure:
1

Main Menu (text = '')

CON What would you want to check
1. My Account
2. My phone number
2

Option 1: Account Menu (text = '1')

CON Choose account information you want to view
1. Account number
3

Option 1-1: Show Account (text = '1*1')

END Your account number is ACC1001
Session ends.
4

Option 2: Show Phone (text = '2')

END Your phone number is +254711000111
Session ends.
5

Invalid Input

END Invalid choice
Session ends.

Text Parameter Format

The text parameter shows the user’s navigation path using * as a separator:
User Actiontext ValueMeaning
Dials *123#""First request (main menu)
Selects 1"1"User chose option 1
Then selects 2"1*2"User chose 1, then 2
Then selects 3"1*2*3"User chose 1, then 2, then 3

Building Complex Menus

@ussd_bp.route("/session", methods=["POST"])
def ussd_handler():
    text = request.values.get("text", "")
    phone_number = request.values.get("phoneNumber", None)
    
    if text == "":
        # Main menu
        response = "CON Welcome! Choose a service\n"
        response += "1. Check Balance\n"
        response += "2. Buy Airtime\n"
        response += "3. Help"
    
    elif text == "1":
        # Check balance
        balance = get_user_balance(phone_number)
        response = f"END Your balance is KES {balance}"
    
    elif text == "2":
        # Airtime amounts menu
        response = "CON Select amount\n"
        response += "1. KES 50\n"
        response += "2. KES 100\n"
        response += "3. KES 200"
    
    elif text == "2*1":
        # Buy KES 50 airtime
        send_airtime(phone_number, 50)
        response = "END KES 50 airtime sent to your number"
    
    elif text == "2*2":
        # Buy KES 100 airtime
        send_airtime(phone_number, 100)
        response = "END KES 100 airtime sent to your number"
    
    elif text == "2*3":
        # Buy KES 200 airtime
        send_airtime(phone_number, 200)
        response = "END KES 200 airtime sent to your number"
    
    elif text == "3":
        # Help
        response = "END For support, call 0711000111 or visit www.example.com"
    
    else:
        response = "END Invalid option. Please try again."
    
    return response

Session Status Notifications

USSD Status Callback

Receive end-of-session notifications with session details.
curl -X POST http://localhost:8000/ussd/status \
  -d "date=2025-09-29+12:00:00" \
  -d "sessionId=ATUid_123" \
  -d "serviceCode=*123#" \
  -d "networkCode=63902" \
  -d "phoneNumber=%2B254711000111" \
  -d "status=Success" \
  -d "cost=KES+0.10" \
  -d "durationInMillis=12345" \
  -d "hopsCount=3" \
  -d "input=1*1" \
  -d "lastAppResponse=END+Your+account+number+is+ACC1001"
date
string
required
Timestamp of session end
sessionId
string
required
Session identifier
serviceCode
string
required
USSD shortcode
networkCode
string
required
Mobile network operator code
phoneNumber
string
required
User’s phone number
status
string
required
Session status (e.g., “Success”)
cost
string
required
Session cost
durationInMillis
string
required
Session duration in milliseconds
hopsCount
string
required
Number of user interactions
input
string
required
Full user navigation path
lastAppResponse
string
required
Last response sent to user
errorMessage
string
Error message (if status is not “Success”)
Implementation
# From routes/ussd.py:49-78
ussd_bp.route("/status", methods=["POST"])

def ussd_status():
    payload = {key: request.values.get(key) for key in request.values.keys()}

    print("📲 USSD Status Notification Received:")
    for key, value in payload.items():
        print(f"   {key}: {value}")

    # Always acknowledge with 200 OK
    return Response("OK", status=200)
Response
  • Status: 200 OK
  • Body: "OK"

Testing USSD Locally

Using Simulator

1

Start Local Server

python app.py
2

Expose with ngrok

ngrok http 8000
Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
3

Test with curl

# Main menu
curl -X POST https://abc123.ngrok.io/ussd/session \
  -d "sessionId=test-123" \
  -d "serviceCode=*123#" \
  -d "phoneNumber=%2B254711000111" \
  -d "text="

# Select option 1
curl -X POST https://abc123.ngrok.io/ussd/session \
  -d "sessionId=test-123" \
  -d "serviceCode=*123#" \
  -d "phoneNumber=%2B254711000111" \
  -d "text=1"

# Select option 1, then 1
curl -X POST https://abc123.ngrok.io/ussd/session \
  -d "sessionId=test-123" \
  -d "serviceCode=*123#" \
  -d "phoneNumber=%2B254711000111" \
  -d "text=1*1"
4

Configure Callback URL

In Africa’s Talking dashboard, set your USSD callback URL to:
https://abc123.ngrok.io/ussd/session

Best Practices

Limit menu options to 5-7 items per screen for better usability.
# Good
response = "CON Main Menu\n"
response += "1. Check Balance\n"
response += "2. Buy Airtime\n"
response += "3. Help"

# Avoid
response = "CON Main Menu\n"
response += "1. Check Balance\n"
response += "2. Buy Airtime\n"
response += "3. Transfer Money\n"
response += "4. Pay Bills\n"
response += "5. My Account\n"
response += "6. Settings\n"
response += "7. About\n"
response += "8. Help\n"
response += "9. Exit"
Always tell users what to do next.
# Good
response = "CON Enter amount (10-1000)"

# Avoid
response = "CON Amount:"
Always have a fallback for unexpected input.
# Always end with an else clause
if text == "":
    response = "CON Main Menu..."
elif text == "1":
    response = "CON Option 1..."
else:
    response = "END Invalid option. Please dial again."
USSD sessions timeout after inactivity. Aim for 3-4 menu levels maximum.
# Maximum depth example
if text == "1*2*3*4":
    # This is getting too deep
    response = "END Please dial again to continue"
For complex flows, store session data in a database or cache.
import redis

r = redis.Redis()

# Store user's selection
if text == "1":
    r.setex(f"session:{session_id}", 300, "airtime_flow")
    response = "CON Enter amount\n"

# Retrieve later
elif text.startswith("1*"):
    flow = r.get(f"session:{session_id}")
    if flow == "airtime_flow":
        amount = text.split("*")[1]
        send_airtime(phone_number, amount)
        response = f"END Airtime of {amount} sent"

Next Steps

SMS Service

Send SMS confirmations after USSD transactions

Airtime Service

Integrate airtime purchases in your USSD menu

Build docs developers (and LLMs) love