Skip to main content

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
FieldTypeRequiredDescription
phonestringYesUser phone number in E.164 format
shortcodestringYesUSSD shortcode (e.g. *123#)
messagestringYesMenu text to display (max 182 chars)
session_idstringYesUnique session identifier
dataobjectNoCustom session state to store
timeoutintegerNoSession 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:

KeyAction
0Exit the session
00Go back one level
1-9Select 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 menu in session data so continue knows context.
  • End terminal screens with end_session: true.

Last Updated: May 2026 ยท Need help? Contact Support โ†’