ONTECH
USSD Gateway Docs
Sign In

USSD Gateway Integration Guide

How to build applications that work with the Ontech *388# USSD Gateway

Overview

The Ontech USSD Gateway routes USSD requests from Zambian mobile network operators to your application via HTTP. When a subscriber dials *388*XX#, the gateway forwards the request to your callback URL and returns your response to the subscriber's phone.

DetailValue
Supported MNOsAirtel Zambia, MTN Zambia, Zamtel
Shortcode Range388*XX (sub-codes under *388#)
ProtocolHTTPS POST with JSON body
Response Timeout10 seconds
Max Response Length160 characters recommended

Quick Start

  1. Build a callback endpoint — an HTTP endpoint that accepts POST with JSON
  2. Request a shortcode — contact Ontech for a 388*XX allocation
  3. We configure the route — your URL gets registered in the gateway
  4. Test it — use the USSD Simulator or dial from a real handset
  5. Go live — subscribers dial *388*XX# and reach your app

Request Format

The gateway sends a POST request with a JSON body to your endpoint:

POST https://your-domain.com/api/ussd/callback
Content-Type: application/json

{
    "session_id": "17739829840001234",
    "msisdn": "260971234567",
    "user_input": "388*10",
    "is_new_request": true,
    "mno": "AIRTEL",
    "shortcode": "388*10",
    "request_id": "a2813ed9",
    "session_data": {}
}
FieldTypeDescription
session_idstringUnique session identifier from the MNO. Use this to track the conversation.
msisdnstringSubscriber's phone number in international format (e.g. 260971234567)
user_inputstringFirst request: the dialed shortcode (e.g. 388*10). Follow-up: the user's menu selection (e.g. 1, 2)
is_new_requestbooleantrue = subscriber just dialed (new session). false = subscriber is responding to a menu
mnostringNetwork operator: AIRTEL, MTN, or ZAMTEL
shortcodestringThe matched shortcode route
request_idstringUnique request tracking ID (log this for debugging)
session_dataobjectSession context from previous interactions (empty on first request)

Response Format

Your endpoint must return a JSON response with exactly two fields:

{
    "response_string": "Welcome to My Service\n1. Check Balance\n2. Make Payment\n3. Exit",
    "continue_session": true
}
FieldTypeRequiredDescription
response_stringstringYesThe USSD text to display. Use \n for line breaks.
continue_sessionbooleanYestrue = show menu, wait for input. false = show message, end session.
Always return HTTP 200 with valid JSON containing both fields. Any other response will show "Service error" to the subscriber.

HTTP Headers

The gateway includes these headers with every request:

HeaderDescription
Content-TypeAlways application/json
X-Request-IDUnique request identifier for tracing
X-MNOMobile network operator name
X-MSISDNSubscriber phone number

Conversation Flow Example

Step 1 — User dials *388*10#

// Gateway sends:
{"session_id": "sess_001", "user_input": "388*10", "is_new_request": true, ...}

// Your response:
{"response_string": "Welcome\n1. Check Balance\n2. Pay\n3. Statement", "continue_session": true}

Subscriber sees the menu on their phone and enters 1.

Step 2 — User selects option 1

// Gateway sends:
{"session_id": "sess_001", "user_input": "1", "is_new_request": false, ...}

// Your response:
{"response_string": "Enter your account number:", "continue_session": true}

Step 3 — User enters 12345

// Gateway sends:
{"session_id": "sess_001", "user_input": "12345", "is_new_request": false, ...}

// Your response (final):
{"response_string": "Your balance is ZMW 5,230.00\nThank you!", "continue_session": false}

Session ends because continue_session is false.

Python (Flask)

from flask import Flask, request, jsonify

app = Flask(__name__)
sessions = {}

@app.route('/api/ussd/callback', methods=['POST'])
def ussd_callback():
    data = request.get_json()
    session_id = data['session_id']
    user_input = data['user_input']

    if data['is_new_request']:
        sessions[session_id] = {'step': 'menu'}
        return jsonify({
            'response_string': 'Welcome\n1. Balance\n2. Pay\n3. Exit',
            'continue_session': True
        })

    step = sessions.get(session_id, {}).get('step')

    if step == 'menu' and user_input == '1':
        return jsonify({
            'response_string': 'Your balance is ZMW 1,500.00',
            'continue_session': False
        })

    return jsonify({
        'response_string': 'Thank you for using our service.',
        'continue_session': False
    })

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)

Node.js (Express)

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

const sessions = {};

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

    if (is_new_request) {
        sessions[session_id] = { step: 'menu' };
        return res.json({
            response_string: 'Welcome\n1. Balance\n2. Pay\n3. Exit',
            continue_session: true
        });
    }

    if (sessions[session_id]?.step === 'menu' && user_input === '1') {
        return res.json({
            response_string: 'Your balance is ZMW 1,500.00',
            continue_session: false
        });
    }

    res.json({ response_string: 'Thank you.', continue_session: false });
});

app.listen(8080);

PHP (Laravel)

Route::post('/api/ussd/callback', function (Request $request) {
    $sessionId = $request->input('session_id');
    $userInput = $request->input('user_input');

    if ($request->input('is_new_request')) {
        session([$sessionId => ['step' => 'menu']]);
        return response()->json([
            'response_string' => "Welcome\n1. Balance\n2. Pay",
            'continue_session' => true
        ]);
    }

    if (session("$sessionId.step") === 'menu' && $userInput === '1') {
        return response()->json([
            'response_string' => 'Balance: ZMW 1,500.00',
            'continue_session' => false
        ]);
    }

    return response()->json([
        'response_string' => 'Thank you.',
        'continue_session' => false
    ]);
});

Error Handling

Subscriber SeesCauseFix
"Service error. Please try again."Your endpoint returned non-JSON or crashedAlways return HTTP 200 with valid JSON
"Service timeout."Response took longer than 10 secondsOptimize to respond within 5 seconds
"Service temporarily unavailable"HTTP status was not 200Always return 200, even for app-level errors
No response / blank screenYour server is unreachableCheck server is publicly accessible, URL is correct

Best Practices

Testing

Test your endpoint before going live:

Using cURL

# New session
curl -X POST https://your-domain.com/api/ussd/callback \
  -H "Content-Type: application/json" \
  -d '{
    "session_id": "test_001",
    "msisdn": "260971234567",
    "user_input": "388*10",
    "is_new_request": true,
    "mno": "AIRTEL",
    "shortcode": "388*10",
    "request_id": "test_req_001",
    "session_data": {}
  }'

# Follow-up (user selects 1)
curl -X POST https://your-domain.com/api/ussd/callback \
  -H "Content-Type: application/json" \
  -d '{
    "session_id": "test_001",
    "msisdn": "260971234567",
    "user_input": "1",
    "is_new_request": false,
    "mno": "AIRTEL",
    "shortcode": "388*10",
    "request_id": "test_req_002",
    "session_data": {}
  }'

Using the USSD Simulator

If you have access to the management portal, use the built-in Test USSD page to simulate real USSD interactions with your endpoint.