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โ
| Event | Fired When |
|---|---|
ussd.session_started | A user dials your shortcode |
ussd.response | A user submits input during a session |
ussd.session_ended | A 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"
}
| Field | Description |
|---|---|
event | The event type |
session_id | Identifier for this session |
phone | User's number in E.164 format |
input | The latest user input |
text | Full 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
}
| Field | Type | Description |
|---|---|---|
message | string | Text to display (max 182 chars) |
end_session | boolean | true 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_endednotifications 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_idin your own store keyed to each request. - Handle
expiredcleanly to release any reserved resources.
๐ Related Resourcesโ
Last Updated: May 2026 ยท Need help? Contact Support โ