JustTap
USSD

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.

  1. A customer dials *363*{routingCode}#.
  2. Just-tap sends newSession: true to your endpoint.
  3. On every menu selection, Just-tap sends newSession: false with userInput.
  4. Return sessionState: "Continue" for another menu or sessionState: "End" for a final message.
USSD session flow
USSD session flowA customer dials the USSD code. The Just-tap gateway sends a new-session request to the vendor application, then sends continuation requests for menu selections until the application returns an end response.CustomerJust-tap GatewayVendor applicationDial *363*{routingCode}#POST newSession=true200 { Continue, first menu }Show first menuCustomer selects an optionPOST newSession=false, userInput200 { Continue or End, message }Show next menu or end session
InteractionRequestApplication behaviour
Customer dials the codenewSession: trueInitialise the journey and return the first menu.
Customer selects an optionnewSession: false with userInputLoad the current step, validate input, and return the next menu or final message.
Invalid selectionContinuation with an unexpected entryKeep the current step and return a helpful prompt with Continue.
Journey completes or customer exitsReturn EndShow 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.

On this page