CON/END replies) and the pitfalls are known (session expiry, carrier prompt quirks). Follow it step for step.
1. Where USSD fits
USSD is the only channel that reaches a feature phone with no data plan, and the only session channel that works on 2G coverage. That makes it the primary product surface in regions where smartphone penetration is uneven — West Africa, East Africa, parts of South and Southeast Asia — and a useful fallback channel elsewhere (balance checks when the app is off-device, OTP-less confirmations). The session is synchronous: the subscriber dials a short code, the mobile operator holds a session open, and every keypress round-trips your application for the next screen. Sessions are cheap for the carrier and survive roaming where IP does not. Choose USSD when the subscriber needs a menu of a few choices in under 160 characters per screen, within a session the carrier closes after tens of seconds of inactivity. For anything longer (surveys, rich content), hand off to SMS or a voice call from the USSD screen.2. Provision a short code and point it at Orbit
A short code (the*384*1# the subscriber dials) belongs to a mobile network operator. You get it either directly from the MNO or, more commonly, through a USSD aggregator (Africa’s Talking, Infobip, or a regional equivalent) that resells session access. Provision the code with your aggregator first; nothing in Orbit works until the aggregator has somewhere to send session traffic.
The link between the provisioned code and Orbit is a single callback URL. In your aggregator dashboard, set the session callback to:
CON/END first token, the replayed accumulated input) you conform to here. Re-point the callback or swap aggregators at any time — the menu you build below is independent of which carrier path delivers the session.
3. Build the session flow as a menu tree
Orbit models a USSD flow as a menu tree — screens (nodes) with option branches — persisted as a single definition against your tenant. The engine that walks it is stateless: because the aggregator replays the accumulated *-joined input on every step, the next screen is a deterministic function of the menu and that input. If you already keep menus in the Flows API for other channels, this is the same graph-thinking applied to a keypad-driven tree; the flow recipes guide covers the multi-channel side. For USSD specifically you configure one purpose-fit object — PUT /api/v1/ussd/menu:
root plus every option next resolves before anything is saved — a broken reference returns 422, so a malformed tree can never answer live traffic. GET /api/v1/ussd/menu reads the current definition back under data.menu (or null when none is set).
4. Menus, session timeouts, and terminal responses
Each node renders as one screen. Two rules from the protocol drive the whole interaction model:CONkeeps the session open. A node withoptionsshows the choices and waits for the next keypress. The subscriber sees your numbered list; you see the selection token (key) appended to the accumulated input.ENDcloses the session. A node with nooptions, orfinal: trueeven with options, renders terminally — the carrier closes the session and the interaction is over. Use terminal nodes for the answers the menu exists to deliver (“Your balance is…”, a confirmation, a one-shot notice).
END Invalid selection. on an unmatched input, which is the only behavior that stops a mis-keyed token from replaying forever. Stray * separators are dropped, so dial strings like **1 desync-proof.
A push session (network-initiated) is the inverse — your application opens the session on the subscriber’s handset, for balance prompts, mobile-money confirmations, or feature-phone OTP. It is a tenant-configured provider endpoint plus POST /api/v1/ussd/push; the channel page covers both halves, and the troubleshooting section below includes the push-specific failures.
5. Test before a carrier ever sees traffic
Two layers of testing, both before pointing the aggregator at Orbit. Simulate the tree.POST /api/v1/ussd/simulate runs the same engine as the live callback: pass text — the accumulated *-joined input, empty for the initial dial — and optionally an inline menu to preview an unsaved candidate:
node_id, the pre-split action (CON/END) and message, and the exact raw body the aggregator would receive (e.g. "END Your balance is 1,250 NGN."). Walk every path in a cheap loop: initial dial, one key per branch, the terminal screens, and at least one bad key per menu to confirm the Invalid selection end. This is the regression harness for menu edits — treat a red simulation like a failing test before you re-point the live callback.
Sandbox numbers for the outbound sides. When the flow also triggers a follow-up SMS, email, or voice confirmation (common on the topup branch), exercise that leg in sandbox mode with the sandbox magic numbers: the trailing digit of the recipient deterministically selects the delivery outcome (delivered, undelivered, carrier-reject, expired), so your handler for the follow-up receipt is tested against every terminal state before a real carrier digit runs through it. The simulator above covers the USSD session itself; the magic numbers cover whatever the session kicks off.
6. Troubleshooting
Session expiry mid-flow. Symptom: the subscriber is cut off with a carrier message before finishing. The MNO closed on inactivity. Fix in the tree, not in the network: shorten prompts, reduce branch depth, and verify with the simulator that every leaf is reachable in a handful of keypresses. A flow that requires more than ~4 decisions is structured wrong for USSD. Screens render garbled or truncated on some handsets. Some (especially older) MNO gateways insert their own header/footer into the screen, and the character cap is 182 including brackets. Keepprompt under ~160 chars and avoid decorative separators; re-test against a real handset after every menu edit, not only the simulator.
Bad key loops — a session that never ends. Symptom: an unmatched input keeps re-prompting. Orbit terminates exactly this case (END Invalid selection.), so it returns only when a custom instrumentation layer added re-prompting logic; the first fix is removing that layer and letting the engine end the session. The aggregator replays the bad token on every subsequent callback, so re-prompting loops forever.
Aggregator reaches the callback but gets an error. When no menu is configured, or any unexpected error occurs, Orbit deliberately returns END Service is not available. — a terminal response that closes the session rather than hanging the subscriber. Check GET /api/v1/ussd/menu for a saved definition first; a null data.menu explains most “the dial works but shows an error” reports.
Push sessions return 409 / 422 / 502. 409 means no push provider is configured yet — set PUT /api/v1/ussd/push/config first. 422 means the body is invalid or the pushUrl was rejected by URL safety validation — the URL must be https:// and must not resolve into internal address space. 502 means the aggregator endpoint was unreachable or returned non-2xx; retry it and check the provider’s own dashboard for the rejected signature.
The callback URL works in the aggregator test console but not for live dials. The /callback/:tenantId path segments are easy to swap on dashboard save. Re-paste the exact URL and confirm the tenant id; the path is the whole credential.
See Also
- USSD channel page — the session protocol contract, menu shape reference, and the full endpoint map.
- Flow recipes — reusable flow definitions when a follow-up SMS/email leg joins the USSD session.
- Sandbox magic numbers — deterministic delivery outcomes for the follow-up leg.
- Build your first IVR flow — graph-validation discipline that applies the same way to USSD menu trees.
- USSD API reference — every field on every endpoint used here.