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

FieldTypeRequiredDescription
msisdnstringNoThe dialling subscriber, E.164 without +.
service_codestringNoWhat they dialled — your code or extension.
session_idstringNoStable for the whole session. Key your state on it.
ussd_stringstring | nullNoEverything typed so far, *-separated ("1*2500"). null/empty on the first screen of a session.
networkstringNoRecipient 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

FieldTypeRequiredDescription
responsestringYesThe text to show. Use \n for line breaks. Keep it under ~160 characters — handsets truncate long screens.
end_sessionbooleanYesfalse 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_string carries 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_id with 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.