laravel-ussd maintained by begenius
Laravel USSD
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