USSD callback mode
We POST every step of a live USSD session to your endpoint; your JSON 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)
{
"msisdn": "254712345678",
"service_code": "*384*45#",
"session_id": "ATUid_8d2c31…",
"ussd_string": "1*2500",
"network": "safaricom"
}Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| msisdn | string | No | The dialling subscriber, E.164 without +. |
| service_code | string | No | What they dialled — your code or extension. |
| session_id | string | No | Stable for the whole session. Key your state on it. |
| ussd_string | string | null | No | Everything typed so far, *-separated ("1*2500"). null/empty on the first screen of a session. |
| network | string | No | Recipient carrier (safaricom, airtel, telkom, equitel). |
What you reply
Response body
{
"response": "Confirm loan of KES 2,500?\n1. Yes\n2. No",
"end_session": false
}Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| response | string | Yes | The text to show. Use \n for line breaks. Keep it under ~160 characters — handsets truncate long screens. |
| end_session | boolean | Yes | false keeps the session open for more input; true shows the text and hangs up. |
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
app.post("/ussd", (req, res) => {
const { session_id, ussd_string } = req.body;
const input = (ussd_string ?? "").split("*").filter(Boolean);
if (input.length === 0) {
return res.json({
response: "Welcome to Acme\n1. Check balance\n2. Talk to us",
end_session: false,
});
}
if (input[0] === "1") {
return res.json({
response: "Your balance is KES 12,340. Thank you!",
end_session: true,
});
}
return res.json({
response: "Call us on 0700 000 000. Goodbye!",
end_session: true,
});
});Session-state tips
ussd_stringcarries the whole input trail, so simple flows can be stateless — re-derive the position from the string each step.- For flows with lookups (accounts, amounts), cache per
session_idwith a short TTL (~2 minutes); sessions are short-lived. - The same subscriber can have only one session at a time; a new dial is a new
session_id.