USSD session model
A USSD interaction is a synchronous, session-scoped exchange between a subscriber’s handset and your application, relayed through an aggregator. This page explains the protocol shape, the design invariant that follows from it (a stateless menu engine), the tree validation that invariant makes safe to trust, and what downstream handlers should branch on. For the request-by-request endpoint walkthrough, see the USSD channel page; for the build-it-end-to-end walkthrough, see Build an interactive USSD flow.1. The synchronous session model
The subscriber dials a short code (for example*384*1#). The mobile network opens a session and your aggregator POSTs each keypress to your callback URL. Your application replies with a plain-text body whose first token decides the disposition:
CON— show the message and keep the session open, awaiting more input.END— show the message and terminate the session.
POST /api/v1/ussd/callback/:tenantId is the public webhook that answers those aggregator steps, and POST /api/v1/ussd/simulate runs the same engine offline so you can walk a tree before a carrier sees it.
2. The stateless-engine invariant
On every callback, the aggregator sends the full accumulated input string — each prior keypress joined by* (the initial dial sends an empty string). A subscriber who pressed 1, then 2, delivers text: "1*2" on the third step.
That replay property is what makes the engine stateless. Because every callback carries the complete input history, the next screen is a pure, deterministic function of (menu definition, accumulated input):
root on every callback by consuming one input token per branch, so any replica can answer any step with identical results. The callback only uses sessionId and phoneNumber for telemetry and routing, not for engine state.
3. Menu-tree invariants
The menu definition is validated before it is saved, so a malformed tree can never answer live traffic. The enforced invariants:- Unique node ids —
rootand every optionnextpointer resolve by id, so a duplicate id would make navigation ambiguous. - Resolvable references —
rootand every option’snextmust point to an existing node; a broken reference fails validation with422at save time. - DTMF-only option keys — option
keyvalues match^[0-9#*]+$(digits,#,*). A real handset can only relay the 12-key DTMF set, so an option keyed to a letter would be unreachable on any live session. - Terminal screens — a node with no
options, or withfinal: true, closes the session. Explicitly markingfinalterminates even when options exist. - Bounded tree — up to 200 nodes, at most 12 options per node, prompts capped at 160 characters (a USSD screen holds roughly 182).
END Invalid selection., and extra input after a terminal screen re-renders that screen and closes. The engine never re-prompts — a re-prompt would loop forever, because the aggregator replays the same bad token on the next callback.
4. Why statelessness matters
The USSD session lives entirely in the callback POST plus the synchronous plain-text response. No session persistence exists to lose, so:- Any replica answers any step. Pod restarts, autoscaling, and multi-replica deployments cannot corrupt a session — a callback routed to a different replica resolves identically.
- Failure recovery is cheap. A failed session is re-run by re-dialing; the subscriber follows the same path and lands on the same screens. There is no session store to reconstruct.
- Testing is a full-faithfulness simulation.
/simulateruns the same pure function the live callback runs, so a simulated walk is a real regression harness, not an approximation of one.
5. What to branch on
A USSD session completes over the callback handshake alone. Your downstream handling should key on the disposition token:CON— an intermediate screen. Keep the session open; do not trigger confirmations, receipts, or side effects yet. ACONre-rendered on a retry is harmless.END— the session is closed. Treat this as the completion signal for the branch the subscriber selected; any follow-up (a confirmation SMS, a log record, a status flip) fires exactly once on the terminal screen.
END screens are the idempotent completion point, a subscriber who re-dials and re-keys the same path receives the same terminal message — a replay, not a duplicate action. Keep any side effect an END triggers idempotent on your side (for example, keyed on sessionId), so a carrier-level retry of the final callback cannot double-fire it.
See also
- USSD channel page — endpoint map, menu shape reference, menu push sessions.
- Build an interactive USSD flow — provisioning, testing, and troubleshooting end to end.
- USSD API reference — every field on every endpoint.
- Idempotency and safe retries — the replay-cache convention for the follow-up legs a session may trigger.