Skip to main content

USSD API

USSD endpoints exposed by the Devotel CPaaS API Base path: /api/v1/ussd Endpoint count: 7

title: “Worked USSD session samples” description: “Worked samples for the USSD session lifecycle: define a menu tree, handle the aggregator session callback with CON and END replies, dry-run a step in the simulator, and the errors to expect. Each sample shows the request and the response together.”

Worked USSD session samples

USSD is not a message channel — it is a session channel. The subscriber dials a short code (for example *384*1#), the aggregator POSTs each step of the session to your callback URL, and your synchronous HTTP response is the next screen: a text/plain body that starts with CON (keep the session open, render the menu) or END (show a final message and terminate). Samples on this page therefore pair the aggregator’s callback and your reply body — one is meaningless without the other. For the interactive builder surface, see the USSD flow builder guide. Bodies are UTF-8 — menus routinely carry non-English prompts in the regions where USSD reaches feature phones, and any characters a GSM handset can render are valid in a prompt.

1. Define the session flow

PUT /api/v1/ussd/menu
A session flow is one menu tree: unique node ids, a root the initial dial renders, and option branches whose next points at another node. A node with no options — or with final: true — is a terminal screen (END); anything else renders its choices and continues (CON). Request
The Swahili prompts above are ordinary UTF-8 in the body — no special encoding is needed.
The tree is validated before anything is saved — duplicate node ids, a root naming no node, or an option next pointing at a missing node all return 422 (see step 4), so a broken graph never answers live sessions.

2. Handle a session callback

POST /api/v1/ussd/callback/{tenantId}
The public callback your aggregator (e.g. Africa’s Talking) POSTs to on every step of a session. On the initial dial the accumulated text is empty and the reply renders the root menu; on each later step the aggregator replays the full *-joined input and the reply renders the screen that input resolves to. Read the request and response together — the response is the screen. Aggregator request (form-encoded or JSON — both are accepted)
Your response — CON renders the next menu and keeps the session open
Text is UTF-8 end to end; the first token of the response body is all the protocol carries (CON / END). Aggregator request — the subscriber pressed 1
Your response — END shows a final message and terminates the session
An unmatched keypress also ends the session — END Invalid selection. Session ended. — which is deliberate: USSD has no “back”, and re-prompting would loop because the aggregator replays the bad token on every callback. Idle sessions are closed by the operator (commonly 30–90 seconds), and because the engine is stateless a subscriber who re-dials and re-keys lands on the same screens — design every path to terminate within a few keypresses.

3. Read the session back

The engine is pure and stateless, so nothing is stored per session and there is no session-history API — “reading a session back” means fetching the flow definition and replaying the aggregator’s accumulated input through the simulator, which runs the same resolution the live callback runs.
GET /api/v1/ussd/menu
Then replay one step — the aggregator’s exact *-joined string — and inspect the node-visit outcome:
POST /api/v1/ussd/simulate
Request
data.raw is the exact text/plain body the live callback would return for that input, and node_id names the node the walk landed on — the per-step node-visit trail. Walk every path this way before pointing the aggregator at the callback: initial dial, one key per branch, each terminal screen, and one deliberately bad key to confirm the invalid-selection end.

4. Errors

422 — the flow graph is invalid. Any structural violation in the tree returns VALIDATION_ERROR and names the offending spot (issue paths point into nodes, options, or root — an option key may only contain digits, #, or *, because a handset can only send the 12-key DTMF set):
Re-send the tree with every next resolving to a node in nodes. Callback timeouts. The callback endpoint answers in milliseconds — there is no slow internal hop, so a hang on the subscriber’s side is almost always the operator’s idle-session timeout rather than a 504-class delay here. Two defensive cases still matter:
  • You publish no menu. A callback for a tenant with no configured menu gets END Service is not available. (HTTP 200) — the session closes gracefully instead of hanging the subscriber at a frozen prompt.
  • GET/PUT /menu times out. Retry with your meta.request_id — reads and writes are idempotent, and a replaced menu takes effect on the next callback.
Keep screens under roughly 160 characters and every path terminating; a menu that waits on input forever is the one shape an idle carrier will punish.

Get the configured USSD menu

GET /api/v1/ussd/menu
Returns the authenticated tenant’s USSD menu definition under data.menu, or null when none has been configured yet.
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.

Get the USSD push-provider config

GET /api/v1/ussd/push/config
Returns the tenant’s network-initiated (push) USSD provider config under data.config, or null when unset. The credential value is never returned — authConfigured reports whether one is stored.
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.

USSD aggregator session callback

POST /api/v1/ussd/callback/{tenantId}
Public webhook the USSD aggregator (e.g. Africa’s Talking) POSTs to on every step of a session. Accepts JSON or form-encoded bodies with sessionId, phoneNumber, serviceCode, and the accumulated text. Responds with a text/plain body beginning CON (continue) or END (terminate). The :tenantId path segment scopes the call to the reseller tenant that owns the menu.
string
required
Platform tenant id that owns the USSD menu.
string
Stripe-style idempotency token. Pass a stable, client-generated value (1-255 chars) to dedupe retries on transient timeouts. The same key+credential+path replays the original response for 24h on 2xx (5min on 4xx, 30s on 5xx). Returns 409 if a concurrent request with the same key is already in flight; replayed responses include the Idempotency-Replay: true response header.
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.

Open a network-initiated (push) USSD session

POST /api/v1/ussd/push
Programmatically opens a USSD session on a subscriber’s handset via the configured aggregator — for balance prompts, mobile-money confirmations and feature-phone OTP flows. action: CON keeps the session open for a reply; END shows a one-shot notice. Requires a push provider configured via PUT /ussd/push/config.
string
Stripe-style idempotency token. Pass a stable, client-generated value (1-255 chars) to dedupe retries on transient timeouts. The same key+credential+path replays the original response for 24h on 2xx (5min on 4xx, 30s on 5xx). Returns 409 if a concurrent request with the same key is already in flight; replayed responses include the Idempotency-Replay: true response header.
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.
string
required
Destination handset in E.164 (e.g. +234801234567).
string
required
First screen text (≤182 chars).
string (enum: CON|END)
CON awaits a reply; END is one-shot. Defaults to CON.
string
Override the default service code.
string
Idempotency / correlation key echoed to the aggregator.

Simulate a USSD session step

POST /api/v1/ussd/simulate
Runs the USSD engine against an text input string (the aggregator’s accumulated *-joined keypresses) and returns the resulting screen, exactly as the live callback would. Pass an inline menu to preview an unsaved definition; otherwise the tenant’s configured menu is used. Useful for building and testing menus before pointing the aggregator at the callback.
string
Stripe-style idempotency token. Pass a stable, client-generated value (1-255 chars) to dedupe retries on transient timeouts. The same key+credential+path replays the original response for 24h on 2xx (5min on 4xx, 30s on 5xx). Returns 409 if a concurrent request with the same key is already in flight; replayed responses include the Idempotency-Replay: true response header.
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.
string
Accumulated input string, *-joined (empty for the initial dial).
object
A USSD menu definition: a tree of nodes (screens) navigated from root.

Create or replace the USSD menu

PUT /api/v1/ussd/menu
Validates and persists the tenant’s USSD menu definition. The menu tree is checked for unique node ids and that root plus every option next resolves to an existing node. Replaces any previously configured menu.
string
Stripe-style idempotency token. Pass a stable, client-generated value (1-255 chars) to dedupe retries on transient timeouts. The same key+credential+path replays the original response for 24h on 2xx (5min on 4xx, 30s on 5xx). Returns 409 if a concurrent request with the same key is already in flight; replayed responses include the Idempotency-Replay: true response header.
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.

Create or replace the USSD push-provider config

PUT /api/v1/ussd/push/config
Validates and persists the aggregator endpoint the platform POSTs network-initiated USSD sessions to. pushUrl must be an https URL; it is re-validated and DNS-pinned against the SSRF guard on every push.
string
Stripe-style idempotency token. Pass a stable, client-generated value (1-255 chars) to dedupe retries on transient timeouts. The same key+credential+path replays the original response for 24h on 2xx (5min on 4xx, 30s on 5xx). Returns 409 if a concurrent request with the same key is already in flight; replayed responses include the Idempotency-Replay: true response header.
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.