USSD callback mode
We POST every step of a live USSD session to your endpoint as form-encoded fields; your plain-text reply is what renders on the handset. One request-response pair per screen.
What we send you
POSThttps://your-app.example.com/ussd (your configured URL)
Request body (per step) — application/x-www-form-urlencoded
sessionId=ATUid_8d2c31
phoneNumber=254712345678
networkCode=1
serviceCode=*657*45*1*2500#
text=1*2500Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| sessionId | string | No | Stable for the whole session. Key your state on it. |
| phoneNumber | string | No | The dialling subscriber, E.164 without +. |
| networkCode | string | No | Carrier code. 1 = Safaricom, 2 = Airtel, 3 = Telkom, 4 = Equitel. |
| serviceCode | string | No | The full dialled string, e.g. *657*45*1*2500#: your shared code with everything the subscriber has entered spliced in. |
| text | string | No | Only what was typed after your extension, *-separated ("1*2500"). Empty on the first screen of a session. |
Form-encoded, not JSON
Fields arrive as
application/x-www-form-urlencoded, the same contract shared USSD has always used, so existing integrations keep working unchanged. Read them the way your framework reads a form post.Verify it came from us
Every step we POST carries
X-MobileSasa-Secret and X-MobileSasa-Signature headers, so your endpoint can reject anyone simulating dials at your URL. The portal's Send test button signs its test exactly like a live step. See Webhook security for verification examples.What you reply
Response body — plain text
CON Confirm loan of KES 2,500?
1. Yes
2. NoParameters
| Field | Type | Required | Description |
|---|---|---|---|
| CON … | prefix | Yes | Keeps the session open: the text is shown and the subscriber can reply. |
| END … | prefix | Yes | Shows the text and hangs up. |
JSON replies also work
A body with no
CON/END prefix is treated as CON. If you prefer JSON, reply{ "response": "…", "end_session": false } and we will render it the same way.Speed matters
Carriers time sessions out fast. Answer in well under 3 seconds: precompute what you can, avoid slow lookups on the hot path. If your endpoint errors or times out, the subscriber sees “Service temporarily unavailable” and the session ends.
A minimal handler
// express.urlencoded() is required: we post form fields, not JSON.
app.use(express.urlencoded({ extended: false }));
app.post("/ussd", (req, res) => {
const { sessionId, phoneNumber, text } = req.body;
const input = (text ?? "").split("*").filter(Boolean);
res.type("text/plain");
if (input.length === 0) {
return res.send("CON Welcome to Mobile Sasa\n1. Check balance\n2. Talk to us");
}
if (input[0] === "1") {
return res.send("END Your balance is KES 12,340. Thank you!");
}
return res.send("END Call us on 0700 000 000. Goodbye!");
});