Skip to main content

Session Management

Learn how to manage USSD sessions in your backend application.

Overview

USSD is session-based - users interact through multiple requests within a single session. Your backend needs to:

  1. Track user state across multiple requests
  2. Store temporary data (inputs, selections, flow position)
  3. Clean up expired sessions
  4. Handle session timeouts

Gateway vs Backend Session Management

What the Gateway Provides

The OLA Gateway provides basic session tracking:

Gateway responsibilities:

  • Creates unique session_id on first dial
  • Maintains session_id across requests
  • Tracks session TTL (default 5 minutes)
  • Auto-expires inactive sessions

What gateway does NOT do:

  • Store your application state
  • Remember user's menu position
  • Track multi-step flows
  • Store user inputs

What Your Backend Must Do

Your backend is responsible for:

  • Storing application state
  • Tracking menu navigation
  • Remembering user inputs
  • Implementing business logic flows

Session Identifiers

Every request includes an ID:

IDPurposeLifetimeUniqueness
session_idTrack the USSD sessionEntire sessionSame across all requests in session
transaction_idTrack individual requestSingle requestUnique for every request

Example Flow

User dials *365#
├─ Request 1: session_id=sess_abc, transaction_id=txn_001
├─ Request 2: session_id=sess_abc, transaction_id=txn_002 (same session)
├─ Request 3: session_id=sess_abc, transaction_id=txn_003 (same session)
└─ Session ends

User dials *365# again (new session)
└─ Request 1: session_id=sess_xyz, transaction_id=txn_004 (new session)

Note that not all providers follow the same binary ID structure. In those cases, the value of the session_id and the transaction_id will be the same. For your backend implementation, the most important tracker is the session_id.

Backend Session Storage

You have several options for storing session state:

Option 1: In-Memory Storage (Simple)

Best for: Development, low-traffic services, single-server deployments

// Node.js example
const sessions = {}; // In-memory store

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

// Get or create session
if (!sessions[session_id]) {
sessions[session_id] = {
step: 'main_menu',
data: {},
created_at: Date.now()
};
}

const session = sessions[session_id];

// Process request
const response = handleRequest(session, input);

// Update session
sessions[session_id] = session;

// Cleanup on end
if (response.end_session) {
delete sessions[session_id];
}

res.json({
session_id,
transaction_id,
output: response.menu,
end_session: response.end_session
});
});

Python example:

sessions = {}  # In-memory store

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

# Get or create session
if session_id not in sessions:
sessions[session_id] = {
'step': 'main_menu',
'data': {},
'created_at': time.time()
}

session = sessions[session_id]

# Process request
response = handle_request(session, user_input)

# Update session
sessions[session_id] = session

# Cleanup on end
if response['end_session']:
del sessions[session_id]

return jsonify({
'session_id': session_id,
'transaction_id': transaction_id,
'output': response['menu'],
'end_session': response['end_session']
})

Pros:

  • Fast
  • Simple
  • No external dependencies

Cons:

  • Lost on restart
  • Not distributed (can't scale horizontally)
  • No persistence

Best for: Production, distributed systems, high traffic

// Node.js with Redis
const redis = require('redis');
const client = redis.createClient();

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

// Get or create session
let session = await client.get(session_id);
if (!session) {
session = {
step: 'main_menu',
data: {},
created_at: Date.now()
};
} else {
session = JSON.parse(session);
}

// Process request
const response = handleRequest(session, input);

// Update session with TTL (5 minutes)
if (!response.end_session) {
await client.setEx(session_id, 300, JSON.stringify(session));
} else {
await client.del(session_id);
}

res.json({
session_id,
transaction_id,
output: response.menu,
end_session: response.end_session
});
});

Python with Redis:

import redis
import json

r = redis.Redis(host='localhost', port=6379, decode_responses=True)

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

# Get or create session
session_data = r.get(session_id)
if session_data:
session = json.loads(session_data)
else:
session = {
'step': 'main_menu',
'data': {},
'created_at': time.time()
}

# Process request
response = handle_request(session, data['input'])

# Update session with TTL (5 minutes)
if not response['end_session']:
r.setex(session_id, 300, json.dumps(session))
else:
r.delete(session_id)

return jsonify({
'session_id': session_id,
'transaction_id': data['transaction_id'],
'output': response['menu'],
'end_session': response['end_session']
})

Pros:

  • Fast (in-memory)
  • Persistent
  • Distributed (multiple servers can share)
  • Built-in TTL (auto-expires sessions)

Cons:

  • Requires Redis server
  • Additional infrastructure

Option 3: Database (PostgreSQL, MySQL)

Best for: Long-term persistence, audit trails, complex queries

// Node.js with PostgreSQL
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});

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

// Get or create session
let result = await pool.query(
'SELECT * FROM ussd_sessions WHERE session_id = $1',
[session_id]
);

let session;
if (result.rows.length === 0) {
// Create new session
await pool.query(
`INSERT INTO ussd_sessions (session_id, msisdn, step, data, created_at)
VALUES ($1, $2, $3, $4, NOW())`,
[session_id, msisdn, 'main_menu', JSON.stringify({})]
);
session = { step: 'main_menu', data: {} };
} else {
session = {
step: result.rows[0].step,
data: JSON.parse(result.rows[0].data)
};
}

// Process request
const response = handleRequest(session, input);

// Update session
if (!response.end_session) {
await pool.query(
`UPDATE ussd_sessions
SET step = $1, data = $2, updated_at = NOW()
WHERE session_id = $3`,
[session.step, JSON.stringify(session.data), session_id]
);
} else {
await pool.query(
'DELETE FROM ussd_sessions WHERE session_id = $1',
[session_id]
);
}

res.json({
session_id,
transaction_id,
output: response.menu,
end_session: response.end_session
});
});

Database Schema:

CREATE TABLE ussd_sessions (
id SERIAL PRIMARY KEY,
session_id VARCHAR(100) UNIQUE NOT NULL,
msisdn VARCHAR(20) NOT NULL,
step VARCHAR(50) NOT NULL,
data JSONB,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_session_id ON ussd_sessions(session_id);
CREATE INDEX idx_msisdn ON ussd_sessions(msisdn);
CREATE INDEX idx_created_at ON ussd_sessions(created_at);

Pros:

  • Persistent
  • Queryable (analytics, debugging)
  • Audit trail
  • Complex data structures

Cons:

  • Slower than in-memory
  • More complex
  • Requires database

Session Data Structure

{
"session_id": "sess_abc123", // From gateway
"provider": "movitel",
"msisdn": "258823456789", // User's phone
"step": "enter_amount", // Current menu/flow step
"data": { // Application-specific data
"selected_service": "transfer",
"recipient": "258821234567",
"amount": null,
"pin": null
},
"history": [ // Navigation history
"main_menu",
"services",
"transfer"
],
"created_at": "2025-01-20T10:30:00Z",
"updated_at": "2025-01-20T10:32:15Z"
}

Key Fields

step

Tracks the user's current position in the flow.

Examples:

"step": "main_menu"           // At main menu
"step": "enter_pin" // Waiting for PIN
"step": "confirm_transfer" // Confirmation screen
"step": "processing" // Processing payment

Use step to route to the correct handler:

function handleRequest(session, input) {
switch (session.step) {
case 'main_menu':
return handleMainMenu(session, input);
case 'enter_amount':
return handleAmountEntry(session, input);
case 'confirm_transfer':
return handleConfirmation(session, input);
default:
return showError();
}
}

data

Stores temporary user inputs and selections.

Examples:

// Transfer flow
{
"data": {
"transfer_type": "mobile",
"recipient": "258821234567",
"amount": "5000",
"reference": "payment for goods"
}
}

// Bill payment
{
"data": {
"account_number": "12345678",
"amount": "1500"
}
}

// Multi-step form
{
"data": {
"name": "John Doe",
"id_number": "123456789",
"email": "john@example.com"
}
}

history

Track navigation for "back" button functionality.

// User navigates: Main → Services → Transfer → Amount
{
"history": ["main_menu", "services", "transfer", "enter_amount"]
}

// User presses "back"
function goBack(session) {
session.history.pop(); // Remove current step
session.step = session.history[session.history.length - 1];
return getMenuForStep(session.step);
}

Session Lifecycle

1. Session Creation (First Dial)

// User dials *365#
// Gateway sends: input = ""

if (!sessions[session_id]) {
sessions[session_id] = {
step: 'main_menu',
data: {},
history: [],
created_at: Date.now()
};
}

2. Session Updates (User Navigation)

// User selects option
// Gateway sends: input = "1"

const session = sessions[session_id];
session.step = 'services';
session.history.push('main_menu');
session.data.last_selection = '1';

3. Session End (User Exits)

// User selects exit or flow completes
// Your backend sends: end_session = true

if (input === '0' || transferComplete) {
delete sessions[session_id]; // Cleanup
return {
menu: ["Thank you!"],
end_session: true
};
}

4. Session Timeout (Inactivity)

The gateway automatically expires sessions after TTL (default 5 minutes).

Your backend should handle:

// Check if session is expired (optional)
function isExpired(session) {
const TTL = 5 * 60 * 1000; // 5 minutes
return (Date.now() - session.created_at) > TTL;
}

if (isExpired(session)) {
delete sessions[session_id];
return {
menu: ["Session expired", "", "Please dial again"],
end_session: true
};
}

Session Cleanup

Manual Cleanup

Remove expired sessions periodically:

// Node.js cleanup job
setInterval(() => {
const now = Date.now();
const TTL = 5 * 60 * 1000; // 5 minutes

for (const [session_id, session] of Object.entries(sessions)) {
if (now - session.created_at > TTL) {
console.log(`Cleaning up expired session: ${session_id}`);
delete sessions[session_id];
}
}
}, 60 * 1000); // Run every minute

Python cleanup:

import threading
import time

def cleanup_sessions():
while True:
now = time.time()
ttl = 5 * 60 # 5 minutes

expired = [
sid for sid, session in sessions.items()
if now - session['created_at'] > ttl
]

for session_id in expired:
print(f'Cleaning up expired session: {session_id}')
del sessions[session_id]

time.sleep(60) # Run every minute

# Start cleanup thread
threading.Thread(target=cleanup_sessions, daemon=True).start()

Redis Auto-Expiry

With Redis, use SETEX for automatic expiry:

// Set with 5-minute TTL
await client.setEx(session_id, 300, JSON.stringify(session));

// Redis automatically deletes after 300 seconds

Database Cleanup

Use a cron job or scheduled task:

-- Delete sessions older than 10 minutes
DELETE FROM ussd_sessions
WHERE updated_at < NOW() - INTERVAL '10 minutes';

Or in application code:

// Run cleanup every 5 minutes
setInterval(async () => {
await pool.query(`
DELETE FROM ussd_sessions
WHERE updated_at < NOW() - INTERVAL '10 minutes'
`);
}, 5 * 60 * 1000);

Best Practices

1. Always Validate Session Data

function validateSession(session) {
if (!session.step) {
console.error('Missing step in session');
session.step = 'main_menu'; // Reset to safe state
}

if (typeof session.data !== 'object') {
console.error('Invalid session data');
session.data = {};
}

return session;
}

2. Handle Missing Sessions Gracefully

let session = await getSession(session_id);
if (!session) {
console.warn(`Session not found: ${session_id}, creating new`);
session = createNewSession(session_id, msisdn);
}

3. Store Minimal Data

// ✅ Good - minimal data
{
"data": {
"amount": "5000",
"recipient": "258821234567"
}
}

// ❌ Bad - too much data
{
"data": {
"user_full_profile": { /* 100 fields */ },
"all_transactions": [ /* 1000 transactions */ ],
"complete_account_history": { /* huge object */ }
}
}

4. Use Consistent Step Names

// ✅ Good - clear naming
"main_menu"
"enter_pin"
"confirm_transfer"
"processing_payment"

// ❌ Bad - unclear
"step1"
"step2"
"confirm"
"proc"

5. Implement Session Recovery

function handleRequest(session, input) {
try {
return routeToHandler(session, input);
} catch (error) {
console.error('Error handling request:', error);

// Reset to safe state
session.step = 'main_menu';
session.data = {};

return {
menu: [
"An error occurred",
"",
"Returning to main menu..."
],
end_session: false
};
}
}

6. Log Session Activity

console.log(JSON.stringify({
timestamp: new Date().toISOString(),
session_id,
msisdn,
step: session.step,
input,
action: 'session_update'
}));

Testing Sessions

Test Session Creation

# First request (creates session)
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 Session Persistence

# Second request (should remember session)
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"
}'

Test Session Cleanup

# End session
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_003",
"input": "0"
}'

# Verify session is deleted
# (check your storage - should be gone)

Next Steps

  • Menu Flows - Build menu navigation using sessions
  • Examples - See real-world session management

Need Help?