Skip to main content
A push (network-initiated) USSD session is the inverse of a dial-in session: your application asks the aggregator to open a session on the subscriber’s handset instead of waiting for the subscriber to dial the short code. This guide takes one from zero to live: configure the provider URL, dispatch the session, make retries safe, and handle the three error classes the endpoint returns by design. The primitives are few (PUT /ussd/push/config, POST /ussd/push) and the pitfalls are known (unconfigured provider, URL-safety rejects, aggregator failures). Follow it step for step.

1. What a push session is

Dial-in USSD begins on the handset — the subscriber dials the short code and the network opens the session. A push session begins at your application: you send the dispatch once, and the subscriber’s handset rings into the menu with the first screen already on it. That is the only primitive behind three workflows the dial-in model cannot do:
  • Balance prompts — push a screen showing a balance or statement straight to the handset, no short code to memorize.
  • Mobile-money confirmations — a payment step that must reach the subscriber immediately, with the menu answer feeding straight back into a callback your application owns.
  • Feature-phone OTP — deliver a one-shot code or a confirmation yes/no to a device that cannot run your app.
Pick END for the one-shot notice (“Your balance is 1,250 NGN.”) or CON for a session that stays open awaiting the subscriber’s reply. The same channel powers both — choose the disposition at dispatch time.

2. Configure the provider

The push provider is your own USSD aggregator endpoint — Africa’s Talking, Infobip, or any HTTP-fronted gateway. You register it per tenant; nothing is global and no company-wide credential exists. Set it once with PUT /api/v1/ussd/push/config:
Two fields matter at write time:
  • pushUrl must be an https:// URL. The validator also rejects URLs that resolve into internal address space, so an aggregator endpoint behind a private hostname fails at write time rather than silently.
  • authHeaderValue is a write-only credential. Read back the config with GET /api/v1/ussd/push/config and the response shows authConfigured: true instead of the secret — the dashboard’s push panel mirrors it the same way (the push card shows the saved endpoint plus an “auth header configured” flag, never the value).
Optional fields: serviceCode sets the default code every push opens against; senderId carries the sender / channel id some aggregators require; authHeaderName picks the header the credential travels in.

3. Dispatch a session

Dispatch one push session per call to POST /api/v1/ussd/push:
phoneNumber accepts E.164 with or without the leading + (normalized on the way in). message is capped at the 182-character USSD payload ceiling. action defaults to CON; send END for the one-shot notice. A successful call returns a 200 envelope with { "status": "accepted", "providerStatus": 200, "phoneNumber": "+234801234567" } — “accepted” is the aggregator confirming receipt, which is as far as the synchronous API can see. The session itself opens on the handset asynchronously after that.

4. Idempotency and retries

Push aggregators retry. Assume a dispatch can arrive twice at the provider and design for it. POST /api/v1/ussd/push accepts clientRequestId — a caller-supplied key echoed to the aggregator in the dispatch envelope — so a replayed request keys on the same value and your side can deduplicate. Key the id on what the session means (bal-20260910-001 above), not on a random nonce per attempt. The “exactly-once per hangup” rule for a push leg means the session lifecycle across retries resolves to one terminal step: a replayed CON that is open must close, not re-open; a replayed END must not re-show. With clientRequestId in the envelope you can assert both from your own callback — a duplicate key means the leg is already in flight or already closed. For END one-shot notices: key the dispatch by a tenant-side id, treat a duplicate END as a no-op, and never fire a second dispatch for the same logical prompt.

5. Sequence with the USSD menu tree

Push and dial-in share the same menu tree — a push session whose action is CON opens on the tree’s root, and the subscriber’s keypad answer routes exactly as it would for a dial-in session. That is how “a menu answer feeds back into a push-driven callback”: the push originated the session; the tree resolves every step after. So a balance prompt that expects a numeric reply goes: POST /ussd/push with action: "CON" opens the root; the subscriber taps 1 for top-up; the aggregator hits your callback (POST /api/v1/ussd/callback/<tenantId> — the same one the USSD flow-builder guide wires for dial-in) with the accumulated input, and your handler confirms the top-up. The push and the menu are two ends of one session — the push begins it, the menu carries it. This sequencing only matters when action is CON. For an END one-shot notice there is no tree step after — no keypad answer exists, and your session side-effects belong on the dispatch call itself.

6. Troubleshooting

Two more shapes worth knowing: A retry that returns a different error class. The call layer orders checks config → body → dispatch, so a second retry may legitimately move from 409 (config once missing, now saved) to 422 (body or URL) or 502 (dispatch) as each layer passes. Triage them in that order, not by which one you saw first. The dashboard push panel as the first check. Before constructing curl retries, open Numbers → USSD (/numbers/ussd) and look at the push card. If it shows no saved endpoint, you have a 409 to fix; if it shows the endpoint, the next failure is almost never the config — take the 422 / 502 split instead.

See Also

  • USSD flow builder — the menu tree dispatch sessions resume against, and the callback the menu answers feed.
  • USSD channel — the session protocol (CON/END first token, accumulated input) and the full endpoint map.
  • USSD API reference — every field on every endpoint used here.