Skip to main content

USSD Callbacks

Callbacks (webhooks) are how AfriRoute notifies your server about USSD activity in real time. In the callback-driven model, the user dials your shortcode directly and AfriRoute forwards every input to your webhook, which responds with the next screen. This is the recommended pattern for production USSD apps.

๐Ÿ”” Callback Eventsโ€‹

EventFired When
ussd.session_startedA user dials your shortcode
ussd.responseA user submits input during a session
ussd.session_endedA session completes, is exited, or expires

โš™๏ธ Configuring Your Callback URLโ€‹

Set your callback URL in the dashboard or per session via the callback_url field. AfriRoute will POST JSON to that URL for each event.

POST https://your-server.com/ussd/callback

๐Ÿ“ฅ Inbound Request Payloadโ€‹

For ussd.session_started and ussd.response, AfriRoute sends:

{
"event": "ussd.response",
"session_id": "sess_abc123",
"phone": "+251911234567",
"shortcode": "*123#",
"input": "2",
"text": "2",
"timestamp": "2026-05-28T10:32:15Z"
}
FieldDescription
eventThe event type
session_idIdentifier for this session
phoneUser's number in E.164 format
inputThe latest user input
textFull accumulated input string for the session

๐Ÿ“ค Expected Responseโ€‹

Reply synchronously with the next screen. Your response body controls what the user sees:

{
"message": "Enter amount to transfer:",
"end_session": false
}
FieldTypeDescription
messagestringText to display (max 182 chars)
end_sessionbooleantrue to display and then close the session

๐Ÿงฉ Handler Examplesโ€‹

const express = require('express');
const app = express();
app.use(express.json());

app.post('/ussd/callback', (req, res) => {
const { session_id, input, event } = req.body;

if (event === 'ussd.session_started') {
return res.json({ message: 'Welcome!\n1. Balance\n2. Transfer\n0. Exit' });
}

if (event === 'ussd.session_ended') {
cleanupSession(session_id);
return res.sendStatus(200);
}

// ussd.response
if (input === '1') {
return res.json({ message: 'Balance: 450 ETB', end_session: true });
}
if (input === '2') {
return res.json({ message: 'Enter recipient phone:', end_session: false });
}
return res.json({ message: 'Invalid option. Try again.\n1. Balance\n2. Transfer' });
});
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.post('/ussd/callback')
def callback():
body = request.get_json()
event = body['event']

if event == 'ussd.session_started':
return jsonify(message='Welcome!\n1. Balance\n2. Transfer\n0. Exit')

if event == 'ussd.session_ended':
cleanup_session(body['session_id'])
return '', 200

if body['input'] == '1':
return jsonify(message='Balance: 450 ETB', end_session=True)
return jsonify(message='Enter recipient phone:', end_session=False)

๐Ÿ Session Ended Payloadโ€‹

{
"event": "ussd.session_ended",
"session_id": "sess_abc123",
"phone": "+251911234567",
"reason": "completed",
"duration": 45,
"total_steps": 5,
"timestamp": "2026-05-28T10:33:00Z"
}

reason is one of completed, user_exit, or expired.

๐Ÿ”’ Verifying Authenticityโ€‹

Each callback includes an X-AfriRoute-Signature header โ€” an HMAC-SHA256 of the raw request body signed with your webhook secret. Verify it before trusting the payload.

const crypto = require('crypto');

function verify(rawBody, signature, secret) {
const expected = crypto.createHmac('sha256', secret)
.update(rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

โฑ๏ธ Timing & Reliabilityโ€‹

  • Respond within 5 seconds โ€” USSD sessions are real-time; slow responses end the session.
  • AfriRoute does not retry response-driven callbacks (they are synchronous), but ussd.session_ended notifications are retried with backoff for up to 24 hours.
  • Make handlers idempotent using session_id.

๐Ÿ’ก Best Practicesโ€‹

  • Respond fast โ€” keep handler work under a second where possible.
  • Always verify the signature before acting.
  • Track state by session_id in your own store keyed to each request.
  • Handle expired cleanly to release any reserved resources.

Last Updated: May 2026 ยท Need help? Contact Support โ†’