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?​