USSD Menus
Menus are the screens a user sees during a USSD session. Each menu is plain text (up to 182 characters) returned by your application. This page covers starting a session with a menu, building multi-level navigation, and designing menus that work well on feature phones.
๐ก Start a Session With a Menuโ
POST /v1/ussd/send
| Field | Type | Required | Description |
|---|---|---|---|
phone | string | Yes | User phone number in E.164 format |
shortcode | string | Yes | USSD shortcode (e.g. *123#) |
message | string | Yes | Menu text to display (max 182 chars) |
session_id | string | Yes | Unique session identifier |
data | object | No | Custom session state to store |
timeout | integer | No | Session timeout in seconds (default 90) |
curl -X POST https://api.afriroute.ai/api/v1/ussd/send \
-H "Authorization: Bearer $AFRIROUTE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+251911234567",
"shortcode": "*123#",
"message": "Welcome to MyBank\n1. Balance\n2. Transfer\n3. Airtime\n0. Exit",
"session_id": "sess_abc123",
"data": { "menu": "main" }
}'
await fetch('https://api.afriroute.ai/api/v1/ussd/send', {
method: 'POST',
headers: {
'Authorization': 'Bearer $AFRIROUTE_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
phone: '+251911234567',
shortcode: '*123#',
message: 'Welcome to MyBank\n1. Balance\n2. Transfer\n3. Airtime\n0. Exit',
session_id: crypto.randomUUID(),
data: { menu: 'main' }
})
});
import requests
requests.post(
'https://api.afriroute.ai/api/v1/ussd/send',
headers={'Authorization': 'Bearer $AFRIROUTE_API_KEY'},
json={
'phone': '+251911234567',
'shortcode': '*123#',
'message': 'Welcome to MyBank\n1. Balance\n2. Transfer\n3. Airtime\n0. Exit',
'session_id': 'sess_abc123',
'data': {'menu': 'main'},
},
)
๐๏ธ Multi-Level Menusโ
Model menus as states and switch on the user's input to pick the next screen. Use /v1/ussd/continue to advance the session.
const MENUS = {
main: 'MyBank\n1. Balance\n2. Transfer\n3. Airtime\n0. Exit',
transfer: 'Transfer\n1. To MyBank\n2. To other bank\n00. Back',
};
function next(input, session) {
switch (session.data.menu) {
case 'main':
if (input === '1') return { message: `Balance: 450 ETB`, end_session: true };
if (input === '2') return { message: MENUS.transfer, data: { menu: 'transfer' } };
if (input === '0') return { message: 'Goodbye!', end_session: true };
break;
case 'transfer':
if (input === '00') return { message: MENUS.main, data: { menu: 'main' } };
return { message: 'Enter recipient phone:', data: { menu: 'transfer_phone' } };
}
// Fallback for unrecognised input
return { message: `Invalid option.\n${MENUS[session.data.menu]}`, data: session.data };
}
๐งญ Navigation Conventionsโ
Use consistent navigation keys across every menu so users learn them quickly:
| Key | Action |
|---|---|
0 | Exit the session |
00 | Go back one level |
1-9 | Select the corresponding option |
โ๏ธ Designing Effective Menusโ
โ
Good โ concise, scannable
MyService
1. Check Balance
2. Buy Airtime
3. Help
0. Exit
โ Bad โ verbose, won't fit
Welcome to MyService where you can do many things such as...
- Stay within 182 characters per screen, including newlines.
- List at most 5-6 options per menu; paginate longer lists.
- Lead with a short title so users know where they are.
- Echo prompts for input (e.g.
Enter amount in ETB:).
๐ Localisationโ
Store the user's language in session data and render the matching menu strings:
const labels = { en: 'Balance', am: 'แแช แแณแฅ', sw: 'Salio' };
const lang = session.data.language || 'en';
const message = `1. ${labels[lang]}\n0. Exit`;
๐ก Best Practicesโ
- Keep the tree shallow โ every extra level loses users.
- Validate input and re-show the same menu on errors instead of ending.
- Persist the current
menuin sessiondatasocontinueknows context. - End terminal screens with
end_session: true.
๐ Related Resourcesโ
Last Updated: May 2026 ยท Need help? Contact Support โ