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*2500

Parameters

FieldTypeRequiredDescription
sessionIdstringNoStable for the whole session. Key your state on it.
phoneNumberstringNoThe dialling subscriber, E.164 without +.
networkCodestringNoCarrier code. 1 = Safaricom, 2 = Airtel, 3 = Telkom, 4 = Equitel.
serviceCodestringNoThe full dialled string, e.g. *657*45*1*2500#: your shared code with everything the subscriber has entered spliced in.
textstringNoOnly 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. No

Parameters

FieldTypeRequiredDescription
CON …prefixYesKeeps the session open: the text is shown and the subscriber can reply.
END …prefixYesShows 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!");
});