Session Flow
Run a first USSD endpoint and understand the customer session lifecycle.
Session Flow
A USSD session begins with a dial request and continues with separate HTTP requests for each customer entry. Use sessionId to associate those requests with one journey.
- A customer dials
*363*{routingCode}#. - Just-tap sends
newSession: trueto your endpoint. - On every menu selection, Just-tap sends
newSession: falsewithuserInput. - Return
sessionState: "Continue"for another menu orsessionState: "End"for a final message.
| Interaction | Request | Application behaviour |
|---|---|---|
| Customer dials the code | newSession: true | Initialise the journey and return the first menu. |
| Customer selects an option | newSession: false with userInput | Load the current step, validate input, and return the next menu or final message. |
| Invalid selection | Continuation with an unexpected entry | Keep the current step and return a helpful prompt with Continue. |
| Journey completes or customer exits | Return End | Show the final message and close the journey. |
Try the contract locally
This minimal Node.js endpoint demonstrates a single menu. It is for local learning only: it does not persist state or verify signatures.
import http from 'node:http';
const menu = 'Welcome to Demo Store\\n1. Opening hours\\n2. Store location\\n0. Exit';
const answer = new Map([['1', 'Demo Store is open Monday to Saturday, 08:00 to 18:00.'], ['2', 'Demo Store is on Example Road, Lusaka.'], ['0', 'Thank you for visiting Demo Store.']]);
http.createServer(async (req, res) => {
if (req.method !== 'POST' || req.url !== '/ussd/webhook') return res.writeHead(404).end();
try {
const chunks = []; for await (const chunk of req) chunks.push(chunk);
const payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));
if (!payload || typeof payload.newSession !== 'boolean' || !payload.sessionId || !payload.msisdn) return res.writeHead(400).end(JSON.stringify({ error: 'Invalid payload' }));
const response = payload.newSession ? { sessionState: 'Continue', message: menu } : answer.has(payload.userInput) ? { sessionState: 'End', message: answer.get(payload.userInput) } : { sessionState: 'Continue', message: `Please select 1, 2 or 0.\\n${menu}` };
res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(response));
} catch { res.writeHead(400).end(JSON.stringify({ error: 'Invalid request' })); }
}).listen(3000, '127.0.0.1');Run it with node ussd-demo.mjs, then send a new session:
curl -i http://127.0.0.1:3000/ussd/webhook -H 'Content-Type: application/json' --data '{"newSession":true,"msisdn":"260970000000","sessionId":"demo-session-001","shortCode":"*363*10#","menuCode":"10","routingCode":"10","provider":"mtn"}'Send a continuation with the same sessionId and userInput: "1" to receive the opening-hours message. These requests simulate the contract only; they do not create a live USSD session. Before exposing an endpoint, implement durable state handling and request verification.
Expiry and response deadline
Sessions expire after 180 seconds of inactivity. The gateway expects valid HTTP 200 JSON within 10 seconds. A timeout, network error, non-200 response, invalid JSON, or missing response fields ends the session with a generic unavailable message.
To confirm before publication: Clarify whether the vendor interface emits expiry, cancellation, or network-abort notifications, and whether mobile-network limits differ from the gateway inactivity window.