An interactive service, step by step
USSD flows
Build a welcome menu, collect a name, fetch sample data, and show a personalised response. You’ll test the flow in the portal’s phone emulator, then publish a version for a sandbox shortcode.
What a flow is
USSD is the menu people reach by dialling a code such as *384*55# on any phone. It needs no data, no app and no smartphone. Each screen is short text; the caller answers by typing a number or a word and pressing Send.
A flow is your service drawn as boxes and lines. Each box is a node: either a screen the caller sees (Menu, List, Input, Confirm, Detail, End) or a step that runs between screens (Request, Branch, Send SMS, Emit event). Each line runs from a dot on one node, such as a menu option, to what happens next. What the caller types is kept in variables, and any screen can show one by writing its name in double braces, like {{name}}.
Screen length. A USSD screen holds about 160 characters, counting the menu numbers, the error line and the Back line. One special character (an emoji, a curly quote) switches the screen to a format that holds less than half as much. The builder counts every screen as you type and won’t publish one that can’t fit.
1. Write your welcome menu
In the business portal, open USSD → Flows, choose Sandbox at the top, and press Start the tutorial (or Create flow with the Tutorial starter). Copy the signing secret it shows once: you need it on your own server later. The flow opens with a welcome menu already on the canvas.
Click the menu. Change its text to your business’s name and add a second option, Say hello. The caller sees the options numbered in order:
2. Ask for their name
Pick Input under Add a node. Set the question to What is your name? and save the answer in the variable name. Drag from the Say hello dot on the menu to the Input. Inputs can check what was typed, such as a PIN, an amount between two values, a date, a phone number or an NRC, and ask again with your error message if it’s wrong.
3. Fetch data from a server
Pick Request and connect the Input’s next dot to it. A Request node posts the variables you choose to a URL, signed so your server knows it came from Switchgear, and keeps the values your server sends back. The caller sees nothing while it runs; it has 1.5 seconds.
- URL: in sandbox, use the echo,
https://sandbox.switchgear.app/v1/sandbox/ussd-echo. It checks the signature like a real server and answers with sample data. Later, your own server’s address. - Send:
name. - Keep from the reply:
greetingandbalance.
A Request node has four dots: ok (your server answered), empty (it answered with an empty list), error (an error, a wrong signature, or a reply in the wrong shape) and timeout (no answer in time).
4. Answer using the variables
Pick End, set its text to {{greeting}}! Your balance is {{balance}}. and connect the Request’s ok dot to it. Add a second End, Sorry, please try again later., for error and timeout. An End screen shows its text and closes the session.
5. Try it on the phone
Beside the canvas is a phone that runs what’s on the canvas, saved or not, in sandbox. Press Dial, type 2, then a name. Under the phone you’ll see the call Switchgear made to the echo, what it sent (secret values hidden), the reply and how long it took. While the session runs, the node on screen is highlighted on the canvas.
6. Publish
Press Publish. Switchgear checks the flow first: every required dot connected, every screen within the length limit, every variable set before a screen uses it. If something’s wrong, it lists the problems; click one to jump to the node. Publishing freezes that version. Later edits go into a new draft, so nobody already in a session is affected, and you can roll back to an earlier version at any time.
7. Put it on a shortcode
A shortcode is what people dial. In sandbox, open USSD → Shortcodes, add a code, choose Flow and pick your flow (the tutorial can do this for you). Dial it from the phone in the code’s details: this time it runs your published version, exactly as a caller would. For live service, confirm shortcode provisioning and channel activation with the Switchgear team before directing real callers to the flow.
Keys callers press
These work on every flow. You can change them in the flow’s settings.
| Key | What it does |
|---|---|
# | Back to the previous screen. On a long list past its first page, the previous page. |
## | Home: the main menu, keeping what was typed so far. |
* | Next page of a long list or of detail lines. |
999 | Start again from the beginning. On a question, 999 is just an answer. |
*384*55*2# | Extra digits in the dial pick menu options straight away: here, option 2 of the first menu. |
Server requests and responses
A Request node sends a signed JSON POST to your server. Only the variables selected in the node are included. The request below uses illustrative session and flow IDs.
POST https://your-server.example/ussd/balance
Content-Type: application/json
X-Switchgear-Signature: t=1793000000,v1=5c1f…e9
{
"session_id": "0199f7a2-5c1e-7b3d-9a40-6e2f1c8d4b57",
"msisdn": "+260971234567",
"shortcode": "*384*55#",
"flow": { "id": "0199f6d0-4a3b-7c2d-9e1f-6a5b4c3d2e10", "version": 3 },
"node": "request_1",
"vars": { "name": "Mwila" }
}Return a JSON response within 1.5 seconds. For this tutorial, return the greeting and balance:
HTTP/1.1 200 OK
Content-Type: application/json
{
"vars": { "greeting": "Hello Mwila", "balance": "K 1,250.00" }
}The node keeps greeting and balance, follows ok, and fills the final screen’s variables.
Optional response fields
Request nodes also support lists, detail lines, text, and named outputs. Include only the fields your flow needs. Use next only when that output is defined and connected on the node.
HTTP/1.1 200 OK
Content-Type: application/json
{
"vars": { "greeting": "Hello Mwila", "balance": "K 1,250.00" },
"items": [
{ "id": "savings", "label": "Savings", "balance": "K 1,250.00" },
{ "id": "current", "label": "Current", "balance": "K 310.50" }
],
"lines": ["Name: Mwila", "Balance: K 1,250.00"],
"text": "Anything else you want to show",
"next": "declined"
}- vars: values for later screens. Only the names listed under “Keep from the reply” are kept.
- items: a list for a List node (up to 200). When the caller picks one, its id goes into the List’s variable, and its label and other fields into
<variable>_labeland<variable>_<field>. - lines: read-only lines for a Detail node. text: one value. next: the name of a dot to follow instead of ok, if you added extra ones.
Outside sandbox, the URL must use HTTPS and its host must be on the environment’s Allowed hosts list (USSD → Flows). Switchgear never calls private or internal addresses.
Check the signature
Every call carries X-Switchgear-Signature: a timestamp and an HMAC-SHA256 of <timestamp>.<raw body> made with your flow’s signing secret (shown when you created the flow; make a new one from the flow’s row in USSD → Flows). Check it on the exact bytes you received, before parsing the JSON, and refuse calls older than five minutes. A rejected signature makes the flow follow the node’s error path.
import express from 'express';
import { verifyWebhook } from '@brivo/switchgear';
const app = express();
// Keep the raw body: the signature covers its exact bytes.
app.post('/ussd/balance', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8');
if (!verifyWebhook(process.env.SWITCHGEAR_FLOW_SECRET, raw, req.header('X-Switchgear-Signature'))) {
return res.status(401).end(); // the flow follows the node's "error" path
}
const call = JSON.parse(raw);
const name = call.vars.name ?? 'there';
// Answer within 1.5 seconds, or the flow follows "timeout".
res.json({ vars: { greeting: `Hello ${name}`, balance: 'K 1,250.00' } });
});
app.listen(3000);Verify without the SDK · Node.js
Call this helper with the flow secret, the unparsed request body, and the signature header before processing the request.
import { createHmac, timingSafeEqual } from 'node:crypto';
/** X-Switchgear-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>"> */
function verify(secret, rawBody, header, toleranceSeconds = 300) {
const parts = Object.fromEntries((header ?? '').split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!parts.v1 || !Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1, 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
}Verify and respond · PHP
<?php
// X-Switchgear-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
function switchgear_verify(string $secret, string $rawBody, ?string $header, int $tolerance = 300): bool {
if (!$header) return false;
$parts = [];
foreach (explode(',', $header) as $pair) {
[$key, $value] = array_pad(explode('=', $pair, 2), 2, '');
$parts[trim($key)] = trim($value);
}
$t = (int) ($parts['t'] ?? 0);
if ($t === 0 || empty($parts['v1']) || abs(time() - $t) > $tolerance) return false;
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1']);
}
$raw = file_get_contents('php://input'); // the exact bytes, before json_decode
$header = $_SERVER['HTTP_X_SWITCHGEAR_SIGNATURE'] ?? null;
if (!switchgear_verify(getenv('SWITCHGEAR_FLOW_SECRET'), $raw, $header)) {
http_response_code(401); // the flow follows the node's "error" path
exit;
}
$call = json_decode($raw, true);
$name = $call['vars']['name'] ?? 'there';
header('Content-Type: application/json');
echo json_encode(['vars' => ['greeting' => "Hello $name", 'balance' => 'K 1,250.00']]);Words used here
- Caller or subscriber: the person dialling.
- Session: one call, from dialling to the last screen. Idle sessions expire; the timeout depends on the network and shortcode configuration.
- Sandbox: your test environment. Nothing reaches a real network, and nothing is charged.
- Signing secret: a password only Switchgear and your server know, used to prove a call is genuine.
- Draft and version: you edit the draft; publishing turns it into a numbered version that callers get.
Test success, error, and timeout paths in sandbox. When you’re ready to connect your service, replace the echo URL with your own signed endpoint and configure the environment’s allowed hosts.