begenius / laravel-ussd
A Laravel package to build scalable USSD applications and workflows.
Requires
- php: ^8.2|^8.3|^8.4
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/routing: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- illuminate/view: ^11.0|^12.0|^13.0
- nesbot/carbon: ^3.0
Requires (Dev)
- orchestra/testbench: ^10.0
- phpunit/phpunit: ^11.0
Suggests
- ext-redis: PHP extension required by illuminate/redis for Redis connections.
- illuminate/redis: Required to use the Redis session driver (>=11.0).
README
Build production-grade USSD applications in Laravel.
laravel-ussd is a framework-agnostic USSD engine that lets you create telecom-grade USSD services with a clean, declarative API. It supports multiple gateways (Orange, Moov, Africa's Talking, Infobip, etc.) through a driver-based architecture.
Table of Contents
- Presentation
- Installation
- Configuration
- Quick Start
- Menus
- Flows
- Actions
- Sessions
- Drivers
- Simulator
- Testing
- Architecture
- Contributing
- Roadmap
Presentation
What is USSD?
USSD (Unstructured Supplementary Service Data) is a protocol used by GSM phones to communicate with service providers. It's the technology behind *123# codes — used for mobile money, balance checks, and telecom services across Africa and beyond.
What this package does
This package provides a complete framework for building USSD services:
- Menu system — Declarative, tree-based navigation
- Flow engine — Multi-step workflows with state machine
- Session management — Database, Redis, or in-memory storage
- 11 gateway drivers — Orange, Moov, Africa's Talking, Infobip, Twilio, Beem, Advanta, Hubtel, MTN, Vodacom, Airtel
- Rate limiting — Per phone number throttling with END 429 response
- Multi-language — Built-in EN/FR with easy extensibility
- Validation — Input validation per flow step
- Logging — Request/response logging
- Simulator — Web-based testing tool
- Artisan commands —
ussd:listandussd:clean - Error handling — Graceful error recovery
Architecture overview
┌──────────────────┐
│ Telephone │
└────────┬─────────┘
│ USSD
┌────────▼─────────┐
│ USSD Gateway │
│(Orange, Moov, AT)│
└────────┬─────────┘
│ HTTP POST
┌────────▼─────────┐
│ UssdController │
└────────┬─────────┘
│
┌────────▼─────────┐
│ UssdEngine │
│ (Orchestrator) │
└──┬────┬────┬─────┘
│ │ │
┌────────┘ │ └────────┐
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────┐
│ Session │ │ Menu │ │ Flow │
│ Manager │ │ Manager │ │ Engine │
└─────────┘ └──────────┘ └──────────┘
Installation
composer require begenius/laravel-ussd
Laravel will auto-discover the service provider. If you're using Laravel < 5.5, add this to config/app.php:
'providers' => [ BeGenius\Ussd\UssdServiceProvider::class, ],
Publish configuration
php artisan vendor:publish --tag=ussd-config
Run migrations
php artisan vendor:publish --tag=ussd-migrations php artisan migrate
Configuration
Configure the package in config/ussd.php or via environment variables:
USSD_DRIVER=default USSD_SESSION_DRIVER=database USSD_SESSION_LIFETIME=2 USSD_REDIS_CONNECTION=default USSD_ROUTES_PREFIX=ussd USSD_SIMULATOR_ENABLED=false USSD_LOGGING_ENABLED=true
| Option | Default | Description |
|---|---|---|
default_driver |
default |
USSD gateway driver |
session_driver |
database |
Session storage driver (database, redis, array) |
session_lifetime |
2 |
Session timeout (minutes) |
redis_connection |
default |
Redis connection name for redis session driver |
session_table |
ussd_sessions |
Database table name |
routes_prefix |
ussd |
URL prefix for USSD routes |
default_menu |
welcome |
Initial menu name |
max_input_length |
182 |
Max input characters |
simulator_enabled |
false |
Enable web simulator |
Quick Start
1. Define menus in a Service Provider
<?php namespace App\Providers; use BeGenius\Ussd\Facades\Ussd; use Illuminate\Support\ServiceProvider; class UssdServiceProvider extends ServiceProvider { public function boot(): void { Ussd::menu('welcome') ->title("Welcome to MyService") ->option('1', 'Check Balance') ->option('2', 'Transfer Money') ->option('3', 'Help', nextMenu: 'help'); } }
2. Configure your gateway
Set your USSD gateway to send POST requests to:
https://your-domain.com/ussd/callback
3. The gateway sends payloads like
{
"sessionId": "ABC123",
"phoneNumber": "22670000000",
"network": "ORANGE",
"text": "1"
}
Your application handles the rest.
Menus
Menus are the building blocks of your USSD application. They display options and handle navigation.
API
use BeGenius\Ussd\Facades\Ussd; Ussd::menu('main') ->title("Main Menu") ->option('1', 'Balance', BalanceAction::class) ->option('2', 'Transfer', TransferFlow::class) ->option('3', 'Settings', nextMenu: 'settings') ->option('0', 'Exit');
Sub-menus
Ussd::menu('settings') ->title("Settings") ->option('1', 'Language', LanguageAction::class) ->option('0', 'Back', nextMenu: 'main');
Navigation flow
- Action: An action class (invokable) executes business logic
- Flow: A multi-step workflow
- nextMenu: Navigate to another menu
- No action/no nextMenu: Re-renders the current menu
Rendering
Menus are auto-rendered as CON responses with numbered options:
CON Main Menu
1. Balance
2. Transfer
3. Settings
0. Exit
Flows
Flows handle multi-step transactions like money transfers.
Creating a Flow
namespace App\Ussd\Flows; use BeGenius\Ussd\Core\UssdContext; use BeGenius\Ussd\Flows\Flow; use BeGenius\Ussd\Flows\Step; use BeGenius\Ussd\Flows\StepResult; use BeGenius\Ussd\Responses\UssdResponse; class TransferFlow extends Flow { public function __construct() { parent::__construct('transfer', 'ask_recipient'); $this->addStep(new class extends Step { public function name(): string { return 'ask_recipient'; } public function handle(UssdContext $context): StepResult { return StepResult::next( 'ask_amount', UssdResponse::continue('Enter recipient phone:') ); } }); $this->addStep(new class extends Step { public function name(): string { return 'ask_amount'; } public function validate(UssdContext $context): ?string { $amount = $context->input(); if (!is_numeric($amount) || $amount <= 0) { return 'Invalid amount. Enter a positive number.'; } return null; } public function handle(UssdContext $context): StepResult { $context->session()->set('amount', $context->input()); return StepResult::next( 'confirm', UssdResponse::continue( "Confirm transfer:\n". "To: {$context->session()->get('recipient')}\n". "Amount: {$context->input()} FCFA\n". "1. Confirm\n". "2. Cancel" ) ); } }); $this->addStep(new class extends Step { public function name(): string { return 'confirm'; } public function handle(UssdContext $context): StepResult { if ($context->input() === '1') { // Process the transfer... return StepResult::complete( UssdResponse::end('Transfer successful!') ); } return StepResult::complete( UssdResponse::end('Transfer cancelled.') ); } }); } }
Register a flow in a menu
Ussd::menu('main') ->option('2', 'Transfer Money', TransferFlow::class);
Or register it independently:
$flow = new TransferFlow(); Ussd::registerFlow($flow);
Actions
Actions are invokable classes that handle a single menu selection.
namespace App\Ussd\Actions; use BeGenius\Ussd\Core\UssdContext; use BeGenius\Ussd\Responses\UssdResponse; class BalanceAction { public function __invoke(UssdContext $context): UssdResponse { $balance = $this->getBalance($context->session()->phoneNumber()); return UssdResponse::end("Your balance is: {$balance} FCFA"); } private function getBalance(string $phoneNumber): float { // Query your database or API return 15000.00; } }
You can also use closures:
Ussd::menu('main') ->option('1', 'Balance', function (UssdContext $context) { return UssdResponse::end('Your balance is 5000 FCFA'); });
Sessions
Sessions persist user state across USSD requests. The package supports three drivers.
Session data
Store temporary data during flows:
$context->session()->set('recipient', '22671234567'); $context->session()->set('amount', '5000'); $recipient = $context->session()->get('recipient'); $hasAmount = $context->session()->has('amount'); $context->session()->forget('amount');
Session lifecycle
- Created — When a user dials the service code
- Active — During menu navigation and flow execution
- Expired — After
session_lifetimeminutes of inactivity - Destroyed — After successful END response
Drivers
USSD Gateway Drivers
Built-in gateway drivers
| Driver | Class | Gateways |
|---|---|---|
default |
DefaultUssdDriver |
Generic (use as fallback) |
orange |
OrangeDriver |
Orange CI, Orange SN |
moov |
MoovDriver |
Moov Africa |
africastalking |
AfricasTalkingDriver |
Africa's Talking |
infobip |
InfobipDriver |
Infobip |
twilio |
TwilioDriver |
Twilio |
beem |
BeemDriver |
Beem (Tanzania) |
advanta |
AdvantaDriver |
Advanta |
hubtel |
HubtelDriver |
Hubtel (Ghana) |
mtn |
MtnDriver |
MTN |
vodacom |
VodacomDriver |
Vodacom (DRC) |
airtel |
AirtelDriver |
Airtel |
Set the driver in .env:
USSD_DRIVER=orange
Custom gateway driver
Create a driver for your specific gateway:
namespace App\Ussd\Drivers; use BeGenius\Ussd\Contracts\UssdDriver; use BeGenius\Ussd\Http\Requests\UssdRequest; use Illuminate\Http\Request; class OrangeDriver implements UssdDriver { public function parseRequest(Request $request): UssdRequest { return new UssdRequest( sessionId: $request->input('sessionId'), phoneNumber: $request->input('msisdn'), network: 'ORANGE', text: $request->input('ussdText', ''), raw: $request->all(), serviceCode: $request->input('serviceCode'), ); } }
Register your driver in the service provider:
// In UssdServiceProvider registration $this->app->bind(UssdDriverContract::class, function ($app) { return new OrangeDriver(); });
Session Drivers
Built-in session drivers:
| Driver | Class | Use case |
|---|---|---|
database |
DatabaseSessionDriver |
MySQL/PostgreSQL (production) |
redis |
RedisSessionDriver |
High-performance with TTL (production) |
array |
ArraySessionDriver |
In-memory (testing only) |
Switch driver via .env:
USSD_SESSION_DRIVER=redis USSD_REDIS_CONNECTION=default
The Redis driver stores sessions as JSON with a configurable TTL (session_lifetime) and supports custom Redis connections.
Simulator
Test your USSD application without a real gateway:
USSD_SIMULATOR_ENABLED=true
Open http://localhost:8000/ussd/simulator in your browser.
┌─────────────────────┐
│ USSD Simulator │
│ │
│ CON Welcome │
│ 1. Balance │
│ 2. Transfer │
│ │
├─────────────────────┤
│ [Input: 1 ] [→] │
│ │
│ Session: abc123 │
└─────────────────────┘
⚠️ Disable in production:
USSD_SIMULATOR_ENABLED=false
Artisan Commands
ussd:list
List all registered menus and their options:
php artisan ussd:list
Output:
+----------------+---------+-----------------------+
| Menu | Options | Preview |
+----------------+---------+-----------------------+
| welcome | 3 | 1. Check Balance |
| | | 2. Transfer Money |
| | | 3. Help |
+----------------+---------+-----------------------+
ussd:clean
Purge expired sessions from storage:
php artisan ussd:clean # purge sessions > 2 min old php artisan ussd:clean --minutes=5 # purge sessions > 5 min old
Rate Limiting
Protect your USSD endpoint from abuse. The built-in ThrottleUssdRequests middleware limits by phone number (with IP fallback):
// In a route file Route::post('/ussd/callback', [UssdController::class, 'handle']) ->middleware('throttle:ussd');
Customize limits in AppServiceProvider:
use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Support\Facades\RateLimiter; RateLimiter::for('ussd', function ($job) { return Limit::perMinute(10)->by($job->input('phoneNumber', $job->ip())); });
When the limit is exceeded, the middleware returns a 429 Too Many Requests response with END format.
Multi-language
Built-in translations for English and French.
__('ussd::ussd.session_expired'); // en: "Session expired. Please dial again." // fr: "Session expirée. Veuillez recomposer le code."
Switch locale as usual:
app()->setLocale('fr');
Publish and customise:
php artisan vendor:publish --tag=ussd-lang
Available keys:
| Key | English | French |
|---|---|---|
welcome |
Welcome | Bienvenue |
back |
Back | Retour |
confirm |
Confirm | Confirmer |
cancel |
Cancel | Annuler |
session_expired |
Session expired... | Session expirée... |
system_error |
A system error... | Une erreur système... |
too_many_requests |
Too many requests... | Trop de requêtes... |
Testing
composer test
The package uses PHPUnit with Orchestra Testbench.
public function it_creates_a_continue_response(): void { $response = UssdResponse::continue("Welcome\n1. Balance"); $this->assertTrue($response->isContinue()); $this->assertEquals('CON', $response->type()); $this->assertEquals("CON Welcome\n1. Balance\n", $response->toString()); }
Architecture
src/
├── UssdServiceProvider.php # Laravel service provider
├── Facades/
│ └── Ussd.php # Facade for UssdEngine
├── Config/
│ └── ussd.php # Configuration file
├── Core/
│ ├── UssdEngine.php # Main orchestrator
│ ├── UssdSession.php # Session value object
│ └── UssdContext.php # Request context
├── Http/
│ ├── Controllers/
│ │ ├── UssdController.php # Gateway callback controller
│ │ └── SimulatorController.php # Web simulator
│ ├── Middleware/
│ │ └── ThrottleUssdRequests.php # Rate limiter middleware
│ └── Requests/
│ └── UssdRequest.php # Parsed USSD request
├── Responses/
│ └── UssdResponse.php # CON/END response builder
├── Menus/
│ ├── Menu.php # Menu definition
│ └── MenuOption.php # Single menu option
├── Flows/
│ ├── Flow.php # Multi-step workflow
│ ├── Step.php # Single flow step
│ └── StepResult.php # Step execution result
├── Contracts/
│ ├── UssdDriver.php # Gateway driver interface
│ └── SessionDriver.php # Session storage interface
├── Drivers/
│ ├── Gateway/ # 11 gateway driver files
│ └── Session/
│ ├── DatabaseSessionDriver.php # DB session storage
│ ├── RedisSessionDriver.php # Redis session storage
│ └── ArraySessionDriver.php # In-memory session
├── Services/
│ ├── SessionManager.php # Session lifecycle
│ └── MenuManager.php # Menu registry
├── Exceptions/
│ ├── UssdException.php # Base exception
│ ├── InvalidMenuException.php # Menu not found
│ └── SessionExpiredException.php # Session timeout
└── Console/
├── Commands/
│ ├── UssdListCommand.php # ussd:list
│ └── UssdCleanCommand.php # ussd:clean
└── Resources/
└── lang/ # Translation files (EN/FR)
Key design patterns
| Pattern | Usage |
|---|---|
| Orchestration | UssdEngine coordinates components |
| Builder | Fluent menu/flow API |
| Registry | MenuManager stores menus by name |
| State | Flow steps as state machine |
| Strategy | Drivers interchangeable via interface |
| Context | UssdContext carries state through pipeline |
| Value Object | UssdRequest, UssdResponse, UssdSession |
Contributing
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Commit changes:
git commit -am 'Add my feature' - Push:
git push origin feature/my-feature - Open a Pull Request
Please follow PSR-12 coding standards and include tests.
Roadmap
v1.0
- Menu system with fluent API
- Multi-step flow engine (state machine)
- Session management (database + Redis + array)
- 11 gateway drivers (Orange, Moov, AT, Infobip, Twilio, Beem, Advanta, Hubtel, MTN, Vodacom, Airtel)
- Rate limiting per phone number
- Multi-language support (EN/FR)
- Artisan commands (
ussd:list,ussd:clean) - CSRF exclusion auto-configuration
- USSD simulator
- Request/response logging
- Error handling
- PHPUnit test suite
- GitHub Actions CI
v1.1
- Visual flow designer UI
- Analytics dashboard
- Webhook integration
- REST API for session management
- Broadcast messages
- Push USSD (MT sessions)
v2.0
- Visual flow designer UI
- Webhook integration
- REST API for session management
- Push USSD (MT sessions)
- Laravel Livewire component
License
laravel-ussd is open-source software licensed under the MIT license.
Built with ❤️ by BeGenius