Documentation Index Fetch the complete documentation index at: https://mintlify.com/mutuiris/voicepact/llms.txt
Use this file to discover all available pages before exploring further.
Overview
USSD (Unstructured Supplementary Service Data) integration provides an interactive menu system for managing contracts without requiring internet connectivity or a smartphone. Users dial a short code to access VoicePact features.
No Internet Required Works on any phone without data or WiFi connection
Interactive Menus Navigate through hierarchical menus with number inputs
Real-Time Updates Check contract status and confirm deliveries instantly
Session Management Maintains context across menu navigation
How It Works
Dial USSD Code
User dials the VoicePact USSD short code (e.g., 384 96#)
See Main Menu
System displays main menu with available options
Navigate Menus
User selects options by entering numbers, navigating through contract lists and details
Take Actions
Perform actions like confirming deliveries or reporting issues
Main Menu
When users dial the USSD code, they see:
Welcome to VoicePact
1. View My Contracts
2. Confirm Delivery
3. Check Payments
4. Help & Support
0. Exit
See ussd.py:309 for menu generation.
View My Contracts
Displays active contracts for the user’s phone number:
📋 Your Contracts:
1. active AG-2024-0012... (active)
2. confirmed AG-2024-0015... (confirmed)
3. pending AG-2024-0019... (pending)
0. Back to Main Menu
Selecting a contract shows details:
Contract Details
active AG-2024-0012...
Product: Maize
Value: KES 350,000
Status: Active
1. Confirm Delivery
2. Report Issue
0. Back
See ussd.py:96 for contract list handler.
Confirm Delivery
Options for delivery confirmation:
Confirm Delivery
Contract: AG-2024-0012...
1. Full Delivery
2. Partial Delivery
3. Report Issue
0. Back
Selecting “Full Delivery” completes the contract:
Full delivery confirmed!
Buyer will be notified.
Payment will be processed.
See ussd.py:251 for delivery handler.
Check Payments
💰 Payment Status
Your last payment: KES 5,000 (Released)
1. View all payments
0. Back to Main Menu
Help & Support
VoicePact Help
Call 0700123456 for support
SMS 'HELP' to 40404
0. Back to Main Menu
USSD Handler Implementation
The main USSD endpoint handles all menu interactions:
@router.post ( "/" )
async def ussd_handler (
request : Request,
sessionId : str = Form( ... ),
serviceCode : str = Form( ... ),
phoneNumber : str = Form( ... ),
text : str = Form( "" ),
at_client : AfricasTalkingClient = Depends(get_africastalking_client),
db : AsyncSession = Depends(get_db)
):
"""Main USSD handler for VoicePact"""
# Get or create session
session = await get_or_create_session(sessionId, phoneNumber, db)
# Parse user input
user_input = text.split( '*' )[ - 1 ] if text else ""
# Route to appropriate menu handler
if not text:
response = main_menu()
session.current_menu = "main"
else :
response = await handle_menu_navigation(
session, user_input, phoneNumber, at_client, db
)
# Update session
session.last_input = user_input
session.last_response = response
await db.commit()
return response
See ussd.py:19 for full implementation.
Session Management
USSD sessions maintain state across menu navigation:
class USSDSession :
session_id: str # Unique session identifier
phone_number: str # User's phone number
current_menu: str # Current menu location
context_data: dict # Menu-specific data
last_input: str # Previous user input
last_response: str # Previous response shown
expires_at: datetime # Session expiration
Session Creation
async def get_or_create_session (
session_id : str ,
phone_number : str ,
db : AsyncSession
) -> USSDSession:
"""Get existing USSD session or create new one"""
result = await db.execute(
select(USSDSession).where(USSDSession.session_id == session_id)
)
session = result.scalar_one_or_none()
if not session:
session = USSDSession(
session_id = session_id,
phone_number = phone_number,
current_menu = "main" ,
context_data = {},
is_active = True ,
expires_at = datetime.utcnow() + timedelta( minutes = 5 )
)
db.add(session)
return session
See ussd.py:357 for session management.
USSD responses use specific prefixes:
CON : Continue session - show menu and wait for input
END : End session - show message and close
def build_ussd_response ( text : str , end_session : bool = False ) -> str :
if end_session:
return f "END { text } "
return f "CON { text } "
See africastalking_client.py:382 for response builder.
Menu handlers process user input based on current menu:
async def handle_menu_navigation (
session : USSDSession,
user_input : str ,
phone_number : str ,
at_client : AfricasTalkingClient,
db : AsyncSession
) -> str :
"""Handle navigation between USSD menus"""
current_menu = session.current_menu
if current_menu == "main" :
return await handle_main_menu(session, user_input, phone_number, at_client, db)
elif current_menu == "contracts" :
return await handle_contracts_menu(session, user_input, phone_number, at_client, db)
elif current_menu == "contract_detail" :
return await handle_contract_detail(session, user_input, phone_number, at_client, db)
elif current_menu == "delivery" :
return await handle_delivery_menu(session, user_input, phone_number, at_client, db)
else :
session.current_menu = "main"
return main_menu()
See ussd.py:64 for navigation logic.
Contract Queries
Retrieve contracts for a phone number:
async def get_user_contracts (
phone_number : str ,
db : AsyncSession
) -> List[Contract]:
"""Get contracts for a phone number"""
result = await db.execute(
select(Contract)
.join(ContractParty)
.where(
and_(
ContractParty.phone_number == phone_number,
Contract.status.in_([
ContractStatus. CONFIRMED ,
ContractStatus. ACTIVE ,
ContractStatus. PENDING
])
)
)
.order_by(Contract.created_at.desc())
)
return result.scalars().all()
See ussd.py:383 for query implementation.
Status Updates
Update contract status from USSD:
async def update_contract_status (
contract_id : str ,
status : ContractStatus,
db : AsyncSession
):
"""Update contract status"""
result = await db.execute(
select(Contract).where(Contract.id == contract_id)
)
contract = result.scalar_one_or_none()
if contract:
contract.status = status
if status == ContractStatus. COMPLETED :
contract.completed_at = datetime.utcnow()
await db.commit()
See ussd.py:405 for status updates.
Test menu generation without dialing:
response = httpx.get(
"https://api.voicepact.com/api/v1/ussd/test/+254712345678"
)
result = response.json()
print ( f "Phone: { result[ 'phone_number' ] } " )
print ( f "Contracts: { result[ 'contracts_count' ] } " )
print ( f " \n Menu: \n { result[ 'ussd_menu' ] } " )
Test Response
{
"phone_number" : "+254712345678" ,
"contracts_count" : 3 ,
"ussd_menu" : "Select Contract: \n 1. AG-2024-001234 - Active (KES 350,000) \n 2. AG-2024-001235 - Confirmed (KES 120,000) \n 3. AG-2024-001236 - Pending (KES 85,000)"
}
See ussd.py:424 for test endpoint.
Status Emojis
Contracts display status indicators:
def get_status_emoji ( status : ContractStatus) -> str :
"""Get emoji for contract status"""
status_emojis = {
ContractStatus. PENDING : "pending" ,
ContractStatus. CONFIRMED : "confirmed" ,
ContractStatus. ACTIVE : "active" ,
ContractStatus. COMPLETED : "completed" ,
ContractStatus. DISPUTED : "disputed" ,
ContractStatus. CANCELLED : "cancelled" ,
ContractStatus. EXPIRED : "expired"
}
return status_emojis.get(status, "unknown" )
See ussd.py:343 for emoji mapping.
Generate contract detail menus:
def contract_detail_menu (
contract : Contract,
at_client : AfricasTalkingClient
) -> str :
"""Generate contract detail menu"""
status_emoji = get_status_emoji(contract.status)
product = contract.terms.get( 'product' , 'Product' )
amount = contract.total_amount or 0
currency = contract.currency
menu_text = f "Contract Details \n "
menu_text += f " { status_emoji } { contract.id[: 12 ] } ... \n "
menu_text += f "Product: { product } \n "
menu_text += f "Value: { currency } { amount :,.0f} \n "
menu_text += f "Status: { contract.status.value.title() } \n\n "
if contract.status == ContractStatus. ACTIVE :
menu_text += "1. Confirm Delivery \n "
menu_text += "2. Report Issue \n "
menu_text += "0. Back"
return at_client.build_ussd_response(menu_text, end_session = False )
See ussd.py:319 for menu generation.
Error Handling
Gracefully handle errors in USSD flow:
try :
response = await handle_menu_navigation(
session, user_input, phoneNumber, at_client, db
)
await db.commit()
return response
except Exception as e:
logger.error( f "USSD handler error: { e } " )
return at_client.build_ussd_response(
"Service temporarily unavailable. Please try again later." ,
end_session = True
)
Best Practices
Store selected items in session
Allow back navigation
Provide breadcrumbs
Clear session on exit
Set 5-minute session expiry
Save progress automatically
Allow resume from last position
Clear expired sessions
Confirm actions immediately
Show clear success/error messages
Notify other parties automatically
Send SMS confirmation
Integration with Africa’s Talking
VoicePact uses Africa’s Talking USSD API:
Service Code : Configured in config.py
Handler Endpoint : POST /api/v1/ussd/
Session Management : Automatic via Africa’s Talking
Response Format : CON/END prefixed text
Database Models
USSD session model:
class USSDSession ( Base ):
__tablename__ = "ussd_sessions"
id : Mapped[ int ] = mapped_column(Integer, primary_key = True )
session_id: Mapped[ str ] = mapped_column(String( 100 ), unique = True , index = True )
phone_number: Mapped[ str ] = mapped_column(String( 20 ), index = True )
current_menu: Mapped[ str ] = mapped_column(String( 50 ), default = "main" )
context_data: Mapped[ dict ] = mapped_column( JSON , default = dict )
last_input: Mapped[Optional[ str ]] = mapped_column(String( 200 ))
last_response: Mapped[Optional[ str ]] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime, default = datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default = datetime.utcnow)
is_active: Mapped[ bool ] = mapped_column(Boolean, default = True )
expires_at: Mapped[datetime] = mapped_column(DateTime, index = True )
See contract.py:312 for model definition.
Common Use Cases
Delivery Confirmation
Farmer delivers goods
Dials USSD code
Selects “Confirm Delivery”
Enters contract ID or selects from list
Confirms full delivery
Buyer notified via SMS
Payment released automatically
Status Check
User dials USSD code
Selects “View My Contracts”
Sees list of active contracts
Selects contract for details
Views current status and amount
Issue Reporting
User discovers problem
Dials USSD code
Navigates to contract
Selects “Report Issue”
Contract marked as disputed
Support team notified
Next Steps
SMS Verification Learn about SMS confirmations
Voice Contracts Create contracts from voice
Mobile Money Integrate payments
Digital Signatures Understand signatures