Skip to main content

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

Need Help?