Integration Examples
Real-world examples of backend applications integrating with OLA USSD Gateway.
Overview
This guide provides complete, production-ready examples in multiple programming languages. Each example demonstrates:
- Session management
- Menu navigation
- Input validation
- Error handling
- External API integration
Note: These examples use simple in-memory storage for clarity. In production, use Redis or a database.
Example 1: Simple Balance Check
A minimal USSD app that checks account balance.
Node.js Implementation
const express = require('express');
const app = express();
app.use(express.json());
// Mock user database
const users = {
'258823456789': { name: 'John Doe', balance: 1234.56 },
'258843456789': { name: 'Jane Smith', balance: 5678.90 }
};
app.post('/ussd/callback', (req, res) => {
const { msisdn, session_id, transaction_id, input } = req.body;
// Get user
const user = users[msisdn];
if (!user) {
return res.json({
session_id,
transaction_id,
output: ["User not found", "", "Please register first"],
end_session: true
});
}
// First dial - show menu
if (!input) {
return res.json({
session_id,
transaction_id,
output: [
`Welcome ${user.name}`,
"",
"1. Check Balance",
"0. Exit"
],
end_session: false
});
}
// Handle selection
if (input === '1') {
return res.json({
session_id,
transaction_id,
output: [
"Account Balance",
"",
`Available: ${user.balance.toFixed(2)} MZN`,
"",
"Thank you!"
],
end_session: true
});
}
if (input === '0') {
return res.json({
session_id,
transaction_id,
output: ["Goodbye!"],
end_session: true
});
}
// Invalid option
res.json({
session_id,
transaction_id,
output: ["Invalid option", "", "Please try again"],
end_session: false
});
});
app.listen(8081, () => {
console.log('USSD backend running on port 8081');
});
Python Implementation
from flask import Flask, request, jsonify
app = Flask(__name__)
# Mock user database
users = {
'258823456789': {'name': 'John Doe', 'balance': 1234.56},
'258843456789': {'name': 'Jane Smith', 'balance': 5678.90}
}
@app.route('/ussd/callback', methods=['POST'])
def ussd_callback():
data = request.json
msisdn = data['msisdn']
session_id = data['session_id']
transaction_id = data['transaction_id']
user_input = data.get('input', '')
# Get user
user = users.get(msisdn)
if not user:
return jsonify({
'session_id': session_id,
'transaction_id': transaction_id,
'output': ['User not found', '', 'Please register first'],
'end_session': True
})
# First dial - show menu
if not user_input:
return jsonify({
'session_id': session_id,
'transaction_id': transaction_id,
'output': [
f"Welcome {user['name']}",
'',
'1. Check Balance',
'0. Exit'
],
'end_session': False
})
# Handle selection
if user_input == '1':
return jsonify({
'session_id': session_id,
'transaction_id': transaction_id,
'output': [
'Account Balance',
'',
f"Available: {user['balance']:.2f} MZN",
'',
'Thank you!'
],
'end_session': True
})
if user_input == '0':
return jsonify({
'session_id': session_id,
'transaction_id': transaction_id,
'output': ['Goodbye!'],
'end_session': True
})
# Invalid option
return jsonify({
'session_id': session_id,
'transaction_id': transaction_id,
'output': ['Invalid option', '', 'Please try again'],
'end_session': False
})
if __name__ == '__main__':
app.run(port=8081, debug=True)
Example 2: Money Transfer with State Management
Complete money transfer flow with session management.
Node.js Implementation
const express = require('express');
const app = express();
app.use(express.json());
// Session storage
const sessions = {};
// Mock users database
const users = {
'258823456789': { name: 'John Doe', balance: 5000.00, pin: '1234' },
'258843456789': { name: 'Jane Smith', balance: 3000.00, pin: '5678' }
};
// Helper functions
function getOrCreateSession(session_id, msisdn) {
if (!sessions[session_id]) {
sessions[session_id] = {
step: 'main_menu',
data: {},
msisdn
};
}
return sessions[session_id];
}
function showMainMenu(user) {
return {
output: [
`Welcome ${user.name}`,
'',
'1. Check Balance',
'2. Transfer Money',
'0. Exit'
],
end_session: false
};
}
function validatePhoneNumber(phone) {
return /^(258)?(82|84|86|87)\d{7}$/.test(phone);
}
function normalizePhone(phone) {
if (phone.startsWith('258')) {
return phone;
}
return '258' + phone;
}
// Main handler
app.post('/ussd/callback', (req, res) => {
const { msisdn, session_id, transaction_id, input } = req.body;
const user = users[msisdn];
if (!user) {
return res.json({
session_id,
transaction_id,
output: ['User not found'],
end_session: true
});
}
const session = getOrCreateSession(session_id, msisdn);
try {
const response = handleRequest(session, input, user);
// Cleanup on end
if (response.end_session) {
delete sessions[session_id];
}
res.json({
session_id,
transaction_id,
...response
});
} catch (error) {
console.error('Error:', error);
delete sessions[session_id];
res.json({
session_id,
transaction_id,
output: ['System error', '', 'Please try again'],
end_session: true
});
}
});
function handleRequest(session, input, user) {
switch (session.step) {
case 'main_menu':
return handleMainMenu(session, input, user);
case 'enter_recipient':
return handleRecipient(session, input);
case 'enter_amount':
return handleAmount(session, input, user);
case 'enter_pin':
return handlePIN(session, input, user);
case 'confirm':
return handleConfirm(session, input, user);
default:
session.step = 'main_menu';
return showMainMenu(user);
}
}
function handleMainMenu(session, input, user) {
if (!input) {
return showMainMenu(user);
}
if (input === '1') {
return {
output: [
'Account Balance',
'',
`Available: ${user.balance.toFixed(2)} MZN`
],
end_session: true
};
}
if (input === '2') {
session.step = 'enter_recipient';
return {
output: ['Transfer Money', '', 'Enter recipient number:'],
end_session: false
};
}
if (input === '0') {
return {
output: ['Goodbye!'],
end_session: true
};
}
return {
output: ['Invalid option', '', 'Try again'],
end_session: false
};
}
function handleRecipient(session, input) {
if (!validatePhoneNumber(input)) {
return {
output: [
'Invalid number',
'',
'Format: 258821234567',
'or: 821234567',
'',
'Enter number:'
],
end_session: false
};
}
const recipient = normalizePhone(input);
if (!users[recipient]) {
return {
output: ['Recipient not found', '', 'Enter valid number:'],
end_session: false
};
}
session.data.recipient = recipient;
session.data.recipient_name = users[recipient].name;
session.step = 'enter_amount';
return {
output: [
`To: ${users[recipient].name}`,
recipient,
'',
'Enter amount (MZN):'
],
end_session: false
};
}
function handleAmount(session, input, user) {
const amount = parseFloat(input);
if (isNaN(amount) || amount <= 0) {
return {
output: ['Invalid amount', '', 'Enter amount:'],
end_session: false
};
}
if (amount > user.balance) {
return {
output: [
'Insufficient balance',
`Available: ${user.balance.toFixed(2)} MZN`,
'',
'Enter amount:'
],
end_session: false
};
}
if (amount > 10000) {
return {
output: ['Transfer limit exceeded', 'Max: 10,000 MZN'],
end_session: false
};
}
session.data.amount = amount;
session.step = 'enter_pin';
return {
output: [
'Transfer Summary',
`To: ${session.data.recipient_name}`,
`Amount: ${amount.toFixed(2)} MZN`,
'',
'Enter PIN:'
],
end_session: false
};
}
function handlePIN(session, input, user) {
if (!/^\d{4}$/.test(input)) {
return {
output: ['Invalid PIN', '', 'Enter 4-digit PIN:'],
end_session: false
};
}
if (input !== user.pin) {
return {
output: ['Incorrect PIN', '', 'Transaction cancelled'],
end_session: true
};
}
session.data.pin = input;
session.step = 'confirm';
return {
output: [
'Confirm Transfer',
'',
`To: ${session.data.recipient_name}`,
session.data.recipient,
`Amount: ${session.data.amount.toFixed(2)} MZN`,
'',
'1. Confirm',
'0. Cancel'
],
end_session: false
};
}
function handleConfirm(session, input, user) {
if (input === '0') {
return {
output: ['Transfer cancelled'],
end_session: true
};
}
if (input === '1') {
// Process transfer
const { recipient, amount } = session.data;
user.balance -= amount;
users[recipient].balance += amount;
const ref = 'TXN' + Date.now();
return {
output: [
'Transfer Successful!',
'',
`Amount: ${amount.toFixed(2)} MZN`,
`To: ${users[recipient].name}`,
`Ref: ${ref}`,
'',
`New balance: ${user.balance.toFixed(2)} MZN`
],
end_session: true
};
}
return {
output: ['Invalid option', '', '1. Confirm or 0. Cancel'],
end_session: false
};
}
app.listen(8081, () => {
console.log('Transfer backend running on port 8081');
});
Example 3: External API Integration
USSD app that calls external REST APIs.
Node.js with Axios
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());
const sessions = {};
// External API configuration
const PAYMENT_API = 'https://api.payment-provider.com';
const API_KEY = process.env.PAYMENT_API_KEY;
app.post('/ussd/callback', async (req, res) => {
const { msisdn, session_id, transaction_id, input } = req.body;
const session = sessions[session_id] || {
step: 'main_menu',
data: {}
};
sessions[session_id] = session;
try {
const response = await handleRequest(session, input, msisdn);
if (response.end_session) {
delete sessions[session_id];
}
res.json({
session_id,
transaction_id,
...response
});
} catch (error) {
console.error('Error:', error);
res.json({
session_id,
transaction_id,
output: [
'Service temporarily unavailable',
'',
'Please try again later'
],
end_session: true
});
}
});
async function handleRequest(session, input, msisdn) {
switch (session.step) {
case 'main_menu':
return handleMainMenu(session, input);
case 'select_biller':
return await handleBillerSelection(session, input, msisdn);
case 'enter_account':
return await handleAccountEntry(session, input);
case 'confirm_payment':
return await handleConfirmation(session, input, msisdn);
default:
session.step = 'main_menu';
return showMainMenu();
}
}
function showMainMenu() {
return {
output: [
'Bill Payment',
'',
'1. Electricity (EDMO)',
'2. Water (FIPAG)',
'3. Internet (TDM)',
'0. Exit'
],
end_session: false
};
}
function handleMainMenu(session, input) {
if (!input) {
return showMainMenu();
}
const billers = {
'1': { id: 'edmo', name: 'EDMO (Electricity)' },
'2': { id: 'fipag', name: 'FIPAG (Water)' },
'3': { id: 'tdm', name: 'TDM (Internet)' }
};
if (input === '0') {
return {
output: ['Goodbye!'],
end_session: true
};
}
if (billers[input]) {
session.data.biller = billers[input];
session.step = 'enter_account';
return {
output: [
billers[input].name,
'',
'Enter account number:'
],
end_session: false
};
}
return {
output: ['Invalid option'],
end_session: false
};
}
async function handleAccountEntry(session, input) {
const accountNumber = input.trim();
if (!/^\d{8,12}$/.test(accountNumber)) {
return {
output: [
'Invalid account number',
'',
'Enter 8-12 digits:'
],
end_session: false
};
}
// Call external API to get bill amount
try {
const response = await axios.post(
`${PAYMENT_API}/bills/lookup`,
{
biller_id: session.data.biller.id,
account_number: accountNumber
},
{
headers: { 'Authorization': `Bearer ${API_KEY}` },
timeout: 5000
}
);
session.data.account_number = accountNumber;
session.data.bill_amount = response.data.amount;
session.data.customer_name = response.data.customer_name;
session.step = 'confirm_payment';
return {
output: [
'Bill Details',
'',
`Customer: ${response.data.customer_name}`,
`Account: ${accountNumber}`,
`Amount: ${response.data.amount.toFixed(2)} MZN`,
'',
'1. Pay Now',
'0. Cancel'
],
end_session: false
};
} catch (error) {
if (error.response && error.response.status === 404) {
return {
output: [
'Account not found',
'',
'Check number and try again'
],
end_session: true
};
}
throw error; // Re-throw for main error handler
}
}
async function handleConfirmation(session, input, msisdn) {
if (input === '0') {
return {
output: ['Payment cancelled'],
end_session: true
};
}
if (input === '1') {
// Process payment via external API
try {
const response = await axios.post(
`${PAYMENT_API}/bills/pay`,
{
biller_id: session.data.biller.id,
account_number: session.data.account_number,
amount: session.data.bill_amount,
payer_msisdn: msisdn
},
{
headers: { 'Authorization': `Bearer ${API_KEY}` },
timeout: 10000 // 10 second timeout for payment
}
);
return {
output: [
'Payment Successful!',
'',
`Amount: ${session.data.bill_amount.toFixed(2)} MZN`,
`Account: ${session.data.account_number}`,
`Ref: ${response.data.reference}`,
'',
'Thank you!'
],
end_session: true
};
} catch (error) {
if (error.response && error.response.status === 402) {
return {
output: [
'Insufficient balance',
'',
'Please top up and try again'
],
end_session: true
};
}
throw error;
}
}
return {
output: ['Invalid option', '', '1. Pay or 0. Cancel'],
end_session: false
};
}
app.listen(8081, () => {
console.log('Bill payment backend running on port 8081');
});
Example 4: Redis Session Management
Production-ready session management with Redis.
Node.js with Redis
const express = require('express');
const redis = require('redis');
const { promisify } = require('util');
const app = express();
app.use(express.json());
// Redis client
const client = redis.createClient({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379
});
const getAsync = promisify(client.get).bind(client);
const setAsync = promisify(client.setex).bind(client);
const delAsync = promisify(client.del).bind(client);
const SESSION_TTL = 300; // 5 minutes
async function getSession(session_id) {
const data = await getAsync(session_id);
return data ? JSON.parse(data) : null;
}
async function saveSession(session_id, session) {
await setAsync(session_id, SESSION_TTL, JSON.stringify(session));
}
async function deleteSession(session_id) {
await delAsync(session_id);
}
app.post('/ussd/callback', async (req, res) => {
const { msisdn, session_id, transaction_id, input } = req.body;
try {
// Get or create session
let session = await getSession(session_id);
if (!session) {
session = {
step: 'main_menu',
data: {},
msisdn,
created_at: new Date().toISOString()
};
}
// Process request
const response = handleRequest(session, input);
// Update session
if (response.end_session) {
await deleteSession(session_id);
} else {
await saveSession(session_id, session);
}
res.json({
session_id,
transaction_id,
...response
});
} catch (error) {
console.error('Error:', error);
res.json({
session_id,
transaction_id,
output: ['System error', '', 'Please try again'],
end_session: true
});
}
});
function handleRequest(session, input) {
// Your menu logic here
// Same as previous examples
}
app.listen(8081, () => {
console.log('Redis backend running on port 8081');
});
Example 5: Database-Backed Sessions
Using PostgreSQL for session storage.
Node.js with PostgreSQL
const express = require('express');
const { Pool } = require('pg');
const app = express();
app.use(express.json());
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});
// Create sessions table
async function initDatabase() {
await pool.query(`
CREATE TABLE IF NOT EXISTS ussd_sessions (
session_id VARCHAR(100) PRIMARY KEY,
msisdn VARCHAR(20) NOT NULL,
step VARCHAR(50) NOT NULL,
data JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
)
`);
// Auto-cleanup old sessions
await pool.query(`
DELETE FROM ussd_sessions
WHERE updated_at < NOW() - INTERVAL '10 minutes'
`);
}
initDatabase();
async function getSession(session_id) {
const result = await pool.query(
'SELECT * FROM ussd_sessions WHERE session_id = $1',
[session_id]
);
if (result.rows.length === 0) {
return null;
}
return {
step: result.rows[0].step,
data: result.rows[0].data
};
}
async function saveSession(session_id, msisdn, session) {
await pool.query(
`INSERT INTO ussd_sessions (session_id, msisdn, step, data)
VALUES ($1, $2, $3, $4)
ON CONFLICT (session_id)
DO UPDATE SET step = $3, data = $4, updated_at = NOW()`,
[session_id, msisdn, session.step, JSON.stringify(session.data)]
);
}
async function deleteSession(session_id) {
await pool.query(
'DELETE FROM ussd_sessions WHERE session_id = $1',
[session_id]
);
}
app.post('/ussd/callback', async (req, res) => {
const { msisdn, session_id, transaction_id, input } = req.body;
try {
let session = await getSession(session_id);
if (!session) {
session = { step: 'main_menu', data: {} };
}
const response = handleRequest(session, input, msisdn);
if (response.end_session) {
await deleteSession(session_id);
} else {
await saveSession(session_id, msisdn, session);
}
res.json({
session_id,
transaction_id,
...response
});
} catch (error) {
console.error('Error:', error);
res.json({
session_id,
transaction_id,
output: ['System error'],
end_session: true
});
}
});
function handleRequest(session, input, msisdn) {
// Your menu logic here
}
app.listen(8081, () => {
console.log('Database backend running on port 8081');
});
Testing Your Backend
Using curl
# Test initial dial
curl -X POST "https://{your-domain-or-public-ip}/ussd/callback \
-H "Content-Type: application/json" \
-d '{
"provider": "movitel",
"msisdn": "258823456789",
"session_id": "test_sess_001",
"transaction_id": "test_txn_001",
"input": ""
}'
# Test menu selection
curl -X POST "https://{your-domain-or-public-ip}/ussd/callback \
-H "Content-Type: application/json" \
-d '{
"provider": "movitel",
"msisdn": "258823456789",
"session_id": "test_sess_001",
"transaction_id": "test_txn_002",
"input": "1"
}'
Testing Script
#!/bin/bash
# test-ussd.sh
SESSION_ID="test_$(date +%s)"
MSISDN="258823456789"
URL="https://{your-domain-or-public-ip}/ussd/callback"
function send_request() {
local input=$1
local txn_id="txn_$(date +%s%N)"
curl -s -X POST $URL \
-H "Content-Type: application/json" \
-d "{
\"msisdn\": \"$MSISDN\",
\"session_id\": \"$SESSION_ID\",
\"transaction_id\": \"$txn_id\",
\"input\": \"$input\"
}" | jq '.'
echo ""
}
# Test flow
echo "=== Initial Dial ==="
send_request ""
sleep 1
echo "=== Select Option 1 ==="
send_request "1"
sleep 1
echo "=== Enter Amount ==="
send_request "5000"
Run with:
chmod +x test-ussd.sh
./test-ussd.sh
Next Steps
- API Contract - Understand request/response formats
- Session Management - Advanced session handling
- Menu Flows - Build complex menu navigation
- API Reference - Complete API documentation