gowelle / flutterwave-php
Modern Laravel package for Flutterwave v4 API integration
Installs: 8
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/gowelle/flutterwave-php
Requires
- php: ^8.2
- illuminate/contracts: ^11.0|^12.0
- illuminate/database: ^11.0|^12.0
- illuminate/http: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
- illuminate/validation: ^11.0|^12.0
- spatie/laravel-package-tools: ^1.92
Requires (Dev)
- laravel/pint: ^1.13
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^2.0
- pestphp/pest-plugin-laravel: ^2.0|^4.0
- phpstan/phpstan: ^1.10
README
A comprehensive Laravel wrapper for Flutterwave Services API v4. This package provides a type-safe, feature-rich integration for Flutterwave payment processing with automatic retry logic, rate limiting, webhook verification, and comprehensive error handling.
Table of Contents
- Features
- Requirements
- Installation
- Quick Start
- Configuration
- Usage
- Charge Sessions
- Events & Listeners
- Webhooks
- Error Handling
- Advanced Usage
- Retry Logic
- Rate Limiting
- Testing
- Troubleshooting
- Static Analysis
- Code Style
- Contributing
- License
- Support
- Changelog
Features
- Complete Flutterwave v4 API Support - Full coverage of Flutterwave's v4 API including payments, refunds, transfers, settlements, and more
- Direct Charge Orchestrator - Simplified payment flow that combines customer, payment method, and charge creation in a single request
- Payment Methods Management - Create, list, and manage payment methods for customers
- Orders API - Complete order management with create, read, update, and list operations
- Bank Operations - Get banks by country, resolve bank accounts, and retrieve bank branches
- Mobile Networks Support - List mobile money networks by country for mobile payments
- Charge Session Tracking - Database-backed tracking of charge sessions with automatic status updates via webhooks
- Event System - Laravel events for direct charge lifecycle and webhook processing
- Automatic Retry Logic - Exponential backoff for transient failures (5xx errors, rate limits, timeouts)
- Rate Limiting - Configurable per-request rate limiting to prevent API quota exhaustion
- Webhook Verification - Secure webhook signature validation with automatic event dispatching
- Type-Safe DTOs - Full TypeScript-like typing with PHP 8.2+ for better IDE support and fewer runtime errors
- Comprehensive Error Handling - Detailed error messages with categorization (validation, authentication, API errors)
- Database Migrations - Built-in migrations for charge session tracking
- Testing Ready - Full test suite with Pest framework and HTTP faking support
- Laravel Integration - Service provider, facade, and comprehensive configuration system
Requirements
- PHP 8.2 or higher
- Laravel 11.0 or 12.0
- Composer
- Flutterwave account with API credentials
Installation
Install the package via Composer:
composer require gowelle/flutterwave-php
The package will automatically register its service provider and facade.
Quick Start
- Publish the configuration file:
php artisan vendor:publish --tag="flutterwave-config"
Or publish all package assets:
php artisan vendor:publish --tag="flutterwave-config" php artisan vendor:publish --tag="flutterwave-migrations"
- Configure your Flutterwave credentials in
.env:
FLUTTERWAVE_CLIENT_ID=your_client_id FLUTTERWAVE_CLIENT_SECRET=your_client_secret FLUTTERWAVE_SECRET_HASH=your_secret_hash FLUTTERWAVE_ENCRYPTION_KEY=your_encryption_key FLUTTERWAVE_ENVIRONMENT=staging # or production
- Verify your credentials:
php artisan flutterwave:verify
- Retrieve your encryption key:
Get your encryption key from your Flutterwave dashboard under API Settings. You'll need this to encrypt card data before sending requests.
- Run migrations (if using charge sessions):
php artisan migrate
- Start using the package:
Important: When making card charge requests, card data must be encrypted using AES-256-GCM encryption. See the Flutterwave Encryption Documentation for encryption requirements and PHP examples.
use Gowelle\Flutterwave\Facades\Flutterwave; // Create a direct charge // NOTE: Card data shown below must be encrypted before sending // See: https://developer.flutterwave.com/docs/encryption $charge = Flutterwave::directCharge()->create([ 'amount' => 1000, 'currency' => 'TZS', 'reference' => 'ORDER-123', 'customer' => [ 'email' => 'customer@example.com', // Required 'first_name' => 'John', // Required 'last_name' => 'Doe', // Required 'phone_number' => '+255123456789', // Required ], 'payment_method' => [ 'type' => 'card', 'card' => [ 'nonce' => 'RANDOMLY_GENERATED_12_CHAR_NONCE', 'encrypted_card_number' => 'BASE64_ENCRYPTED_CARD_NUMBER', 'encrypted_cvv' => 'BASE64_ENCRYPTED_CVV', 'encrypted_expiry_month' => 'BASE64_ENCRYPTED_EXPIRY_MONTH', 'encrypted_expiry_year' => 'BASE64_ENCRYPTED_EXPIRY_YEAR', ], ], 'redirect_url' => 'https://example.com/callback', ]);
Configuration
The package is configured via config/flutterwave.php. After publishing, you can customize all settings:
API Credentials
'client_id' => env('FLUTTERWAVE_CLIENT_ID'), 'client_secret' => env('FLUTTERWAVE_CLIENT_SECRET'), 'secret_hash' => env('FLUTTERWAVE_SECRET_HASH'),
Your Flutterwave API credentials can be found in your Flutterwave dashboard under Settings > API.
Environment
'environment' => env('FLUTTERWAVE_ENVIRONMENT', 'staging'),
Set to 'staging' for testing or 'production' for live transactions.
API Settings
'timeout' => env('FLUTTERWAVE_TIMEOUT', 30), // Request timeout in seconds 'max_retries' => env('FLUTTERWAVE_MAX_RETRIES', 3), // Maximum retry attempts 'retry_delay' => env('FLUTTERWAVE_RETRY_DELAY', 1000), // Retry delay in milliseconds
Rate Limiting
'rate_limit' => [ 'enabled' => env('FLUTTERWAVE_RATE_LIMIT_ENABLED', true), 'max_requests' => env('FLUTTERWAVE_RATE_LIMIT_MAX', 100), 'per_seconds' => env('FLUTTERWAVE_RATE_LIMIT_WINDOW', 60), ],
Configure rate limiting to prevent hitting Flutterwave API limits. The default allows 100 requests per 60 seconds.
Logging
'logging' => [ 'enabled' => env('FLUTTERWAVE_LOGGING_ENABLED', true), 'channel' => env('FLUTTERWAVE_LOG_CHANNEL', 'stack'), 'level' => env('FLUTTERWAVE_LOG_LEVEL', 'info'), 'log_requests' => env('FLUTTERWAVE_LOG_REQUESTS', false), 'log_responses' => env('FLUTTERWAVE_LOG_RESPONSES', false), ],
Control logging behavior. Enable log_requests and log_responses for debugging API interactions.
Webhook Settings
'webhook' => [ 'verify_signature' => env('FLUTTERWAVE_WEBHOOK_VERIFY', true), 'route_path' => env('FLUTTERWAVE_WEBHOOK_PATH', 'webhooks/flutterwave'), 'route_name' => 'flutterwave.webhook', 'middleware' => ['api'], ],
Configure webhook handling. The package automatically registers a webhook route that verifies signatures and dispatches events.
Default Currency
'default_currency' => env('FLUTTERWAVE_DEFAULT_CURRENCY', 'TZS'),
Set the default currency for transactions if not specified in the request.
Charge Sessions
'charge_sessions' => [ 'enabled' => true, 'table_name' => 'flutterwave_charge_sessions', 'cleanup_after_days' => env('FLUTTERWAVE_SESSION_CLEANUP_DAYS', 30), 'auto_create' => env('FLUTTERWAVE_SESSION_AUTO_CREATE', false), 'max_polls' => env('FLUTTERWAVE_SESSION_MAX_POLLS', 60), ],
Configure charge session tracking:
enabled: Enable/disable charge session trackingauto_create: Automatically create sessions when direct charges are createdcleanup_after_days: Days before old sessions are cleaned upmax_polls: Maximum polling attempts for charge status
Cache Settings
'cache' => [ 'enabled' => env('FLUTTERWAVE_CACHE_ENABLED', true), 'prefix' => 'flutterwave', 'ttl' => [ 'access_token' => 3600, // 1 hour (managed by auth service) 'banks' => 86400, // 24 hours 'mobile_networks' => 86400, // 24 hours ], ],
Configure caching for frequently accessed data like access tokens, bank lists, and mobile networks.
Model Classes
'models' => [ 'user' => env('FLUTTERWAVE_USER_MODEL', 'App\Models\User'), 'payment' => env('FLUTTERWAVE_PAYMENT_MODEL', 'App\Domain\Payment\Models\Payment'), ],
Configure the model classes used by the ChargeSession model for relationships. These should be the fully qualified class names of your application's User and Payment models.
Environment Variables Reference
| Variable | Description | Default |
|---|---|---|
FLUTTERWAVE_CLIENT_ID |
Your Flutterwave client ID | - |
FLUTTERWAVE_CLIENT_SECRET |
Your Flutterwave client secret | - |
FLUTTERWAVE_SECRET_HASH |
Your webhook secret hash | - |
FLUTTERWAVE_ENCRYPTION_KEY |
Encryption key for card data | - |
FLUTTERWAVE_ENVIRONMENT |
Environment: staging or production |
staging |
FLUTTERWAVE_DEBUG |
Enable debug logging (dev only) | false |
FLUTTERWAVE_TIMEOUT |
Request timeout in seconds | 30 |
FLUTTERWAVE_MAX_RETRIES |
Maximum retry attempts | 3 |
FLUTTERWAVE_RETRY_DELAY |
Retry delay in milliseconds | 1000 |
FLUTTERWAVE_RATE_LIMIT_ENABLED |
Enable rate limiting | true |
FLUTTERWAVE_RATE_LIMIT_MAX |
Max requests per window | 100 |
FLUTTERWAVE_RATE_LIMIT_WINDOW |
Time window in seconds | 60 |
FLUTTERWAVE_LOGGING_ENABLED |
Enable logging | true |
FLUTTERWAVE_LOG_CHANNEL |
Log channel | stack |
FLUTTERWAVE_LOG_LEVEL |
Log level | info |
FLUTTERWAVE_LOG_REQUESTS |
Log API requests | false |
FLUTTERWAVE_LOG_RESPONSES |
Log API responses | false |
FLUTTERWAVE_WEBHOOK_VERIFY |
Verify webhook signatures | true |
FLUTTERWAVE_WEBHOOK_PATH |
Webhook route path | webhooks/flutterwave |
FLUTTERWAVE_DEFAULT_CURRENCY |
Default currency code | TZS |
FLUTTERWAVE_SESSION_CLEANUP_DAYS |
Days before session cleanup | 30 |
FLUTTERWAVE_SESSION_AUTO_CREATE |
Auto-create charge sessions | false |
FLUTTERWAVE_SESSION_MAX_POLLS |
Max polling attempts | 60 |
FLUTTERWAVE_CACHE_ENABLED |
Enable caching | true |
FLUTTERWAVE_USER_MODEL |
User model class | App\Models\User |
FLUTTERWAVE_PAYMENT_MODEL |
Payment model class | App\Domain\Payment\Models\Payment |
Usage
Direct Charges
The Direct Charge service uses Flutterwave's orchestrator endpoint to simplify the payment flow by combining customer, payment method, and charge creation in a single request.
Important: When making card charge requests, you must encrypt the card information before sending the request. Card data (card number, CVV, expiry month, expiry year) must be encrypted using AES-256-GCM encryption. See the Flutterwave Encryption Documentation for detailed encryption requirements and examples.
Creating a Direct Charge
use Gowelle\Flutterwave\Facades\Flutterwave; use Gowelle\Flutterwave\Exceptions\FlutterwaveException; try { // IMPORTANT: Card data must be encrypted before sending // Retrieve your encryption key from Flutterwave dashboard > API Settings // Use AES-256-GCM encryption with a 12-character nonce // See: https://developer.flutterwave.com/docs/encryption $charge = Flutterwave::directCharge()->create([ 'amount' => 10000, // Amount in smallest currency unit (e.g., cents) 'currency' => 'TZS', // Currency code 'reference' => 'ORDER-123', // Your unique reference 'customer' => [ 'email' => 'customer@example.com', 'name' => 'John Doe', 'phone_number' => '+255123456789', ], 'payment_method' => [ 'type' => 'card', 'card' => [ 'nonce' => 'RANDOMLY_GENERATED_12_CHAR_NONCE', 'encrypted_card_number' => 'BASE64_ENCRYPTED_CARD_NUMBER', 'encrypted_cvv' => 'BASE64_ENCRYPTED_CVV', 'encrypted_expiry_month' => 'BASE64_ENCRYPTED_EXPIRY_MONTH', 'encrypted_expiry_year' => 'BASE64_ENCRYPTED_EXPIRY_YEAR', ], ], 'redirect_url' => 'https://example.com/callback', 'meta' => [ 'order_id' => '12345', 'user_id' => '67890', ], ]); // Check charge status if ($charge->status->isSuccessful()) { // Payment succeeded } elseif ($charge->status->requiresAction()) { // Handle next action (PIN, OTP, redirect, etc.) $nextAction = $charge->nextAction; if ($nextAction->type->requiresCustomerInput()) { // Show PIN or OTP input form } elseif ($nextAction->type->requiresRedirect()) { // Redirect to authorization URL return redirect($nextAction->data['redirect_url']); } } } catch (FlutterwaveException $e) { // Handle error logger()->error('Charge failed', [ 'error' => $e->getUserFriendlyMessage(), 'details' => $e->getErrorData(), ]); }
Updating Charge Authorization
When a charge requires additional authorization (PIN, OTP, AVS), submit the authorization data:
use Gowelle\Flutterwave\Data\AuthorizationData; use Gowelle\Flutterwave\Enums\NextActionType; // For PIN authorization $authorization = AuthorizationData::createPin( nonce: $nonce, // Nonce from Flutterwave encryptedPin: $encryptedPin // Encrypted PIN ); // For OTP authorization $authorization = AuthorizationData::createOtp( code: $otpCode // OTP code from customer ); // For AVS (Address Verification System) $authorization = AuthorizationData::createAvs([ 'line1' => '123 Main St', 'city' => 'Dar es Salaam', 'state' => 'Dar es Salaam', 'country' => 'TZ', 'postal_code' => '11101', ]); // Submit authorization $updatedCharge = Flutterwave::directCharge()->updateChargeAuthorization( chargeId: $charge->id, authorizationData: $authorization ); // Check if charge is now complete if ($updatedCharge->status->isSuccessful()) { // Payment completed successfully } elseif ($updatedCharge->status->requiresAction()) { // May require additional authorization steps }
Checking Charge Status
use Gowelle\Flutterwave\Enums\DirectChargeStatus; $status = Flutterwave::directCharge()->status('charge-id'); if ($status->isSuccessful()) { // Payment succeeded } elseif ($status->isTerminal()) { // Payment failed, cancelled, or timed out } else { // Payment is pending or requires action }
Payments
The Payments service handles the traditional charge flow where you create customers and payment methods separately.
Processing a Payment
use Gowelle\Flutterwave\Facades\Flutterwave; // Process a payment with callback for trace ID $payment = Flutterwave::payments()->process([ 'amount' => 1000, 'currency' => 'TZS', 'reference' => 'ORDER-123', 'customer_id' => 'CUST-456', 'payment_method_id' => 'PM-789', 'payment_method_type' => 'card', 'redirect_url' => 'https://example.com/callback', ], function ($traceId) { // Callback executed when charge is successfully created logger()->info('Charge created', ['trace_id' => $traceId]); }); // Get payment status $status = Flutterwave::payments()->status('charge-id');
Payment Methods
Manage payment methods for customers.
List Payment Methods
$methods = Flutterwave::payments()->methods([ 'customer_id' => 'CUST-456', 'currency' => 'TZS', ]);
Create Payment Method
Important: Card data must be encrypted using AES-256-GCM encryption before sending. See the Flutterwave Encryption Documentation for encryption requirements.
// IMPORTANT: Card data must be encrypted before sending // Retrieve your encryption key from Flutterwave dashboard > API Settings // Use AES-256-GCM encryption with a 12-character nonce // See: https://developer.flutterwave.com/docs/encryption $paymentMethod = Flutterwave::payments()->createMethod([ 'customer_id' => 'CUST-456', 'type' => 'card', 'card' => [ 'nonce' => 'RANDOMLY_GENERATED_12_CHAR_NONCE', 'encrypted_card_number' => 'BASE64_ENCRYPTED_CARD_NUMBER', 'encrypted_cvv' => 'BASE64_ENCRYPTED_CVV', 'encrypted_expiry_month' => 'BASE64_ENCRYPTED_EXPIRY_MONTH', 'encrypted_expiry_year' => 'BASE64_ENCRYPTED_EXPIRY_YEAR', ], ]);
Get Payment Method
$paymentMethod = Flutterwave::payments()->getMethod('payment-method-id');
Customers
Manage customer records.
Create Customer
Required Fields: When creating a customer, the following fields are required:
first_name(orname),last_name(orname), andphone_number.
$customer = Flutterwave::customers()->create([ 'email' => 'john@example.com', // Required 'first_name' => 'John', // Required (or use 'name') 'last_name' => 'Doe', // Required (or use 'name') 'phone_number' => '+255123456789', // Required // Alternative: use 'name' instead of 'first_name' and 'last_name' // 'name' => 'John Doe', ]);
Get Customer
$customer = Flutterwave::customers()->get('customer-id');
List Customers
$customers = Flutterwave::customers()->list([ 'page' => 1, 'limit' => 20, ]);
Orders
Manage orders for tracking purchases and payments.
Create Order
$order = Flutterwave::orders()->create([ 'order_reference' => 'ORDER-123', 'amount' => 10000, 'currency' => 'TZS', 'customer' => [ 'email' => 'customer@example.com', 'name' => 'John Doe', ], 'items' => [ [ 'name' => 'Product 1', 'quantity' => 2, 'unit_price' => 5000, ], ], ]);
Get Order
$order = Flutterwave::orders()->get('order-id');
List Orders
$orders = Flutterwave::orders()->list([ 'page' => 1, 'limit' => 20, ]);
Update Order
$updatedOrder = Flutterwave::orders()->update([ 'id' => 'order-id', 'amount' => 15000, 'status' => 'completed', ]);
Refunds
Process refunds for completed charges.
Create Refund
$refund = Flutterwave::refunds()->create([ 'charge_id' => 'charge-123', 'amount' => 500, 'reason' => 'Customer requested refund', ]);
Get Refund
$refund = Flutterwave::refunds()->get('refund-id');
List Refunds
$refunds = Flutterwave::refunds()->list([ 'charge_id' => 'charge-123', 'page' => 1, 'limit' => 20, ]);
Transfers/Payouts
Send money to bank accounts, mobile money wallets, or Flutterwave wallets.
Bank Transfer (Orchestrator)
The recommended approach - creates the recipient inline:
use Gowelle\Flutterwave\Data\Transfer\BankTransferRequest; $transfer = Flutterwave::transfers()->bankTransfer( new BankTransferRequest( amount: 50000, sourceCurrency: 'NGN', destinationCurrency: 'NGN', accountNumber: '0123456789', bankCode: '044', reference: 'PAYOUT-' . uniqid(), narration: 'Monthly payout', // optional ) );
Mobile Money Transfer (Orchestrator)
use Gowelle\Flutterwave\Data\Transfer\MobileMoneyTransferRequest; $transfer = Flutterwave::transfers()->mobileMoneyTransfer( new MobileMoneyTransferRequest( amount: 1000, sourceCurrency: 'NGN', destinationCurrency: 'GHS', network: 'MTN', phoneNumber: '2339012345678', firstName: 'John', lastName: 'Doe', reference: 'MOMO-' . uniqid(), ) );
Get Transfer
$transfer = Flutterwave::transfers()->get('transfer-id');
List Transfers
$transfers = Flutterwave::transfers()->list();
Retry Failed Transfer
$transfer = Flutterwave::transfers()->retry('transfer-id');
Create Recipient
For the general flow, pre-create recipients:
use Gowelle\Flutterwave\Data\Transfer\CreateRecipientRequest; $recipient = Flutterwave::transfers()->createRecipient( CreateRecipientRequest::bank( currency: 'NGN', accountNumber: '0123456789', bankCode: '044', ) );
Create Sender
use Gowelle\Flutterwave\Data\Transfer\CreateSenderRequest; $sender = Flutterwave::transfers()->createSender( new CreateSenderRequest( firstName: 'John', lastName: 'Doe', email: 'john@example.com', phoneNumber: '+2341234567890', country: 'NG', ) );
Get Transfer Rate
use Gowelle\Flutterwave\Data\Transfer\GetRateRequest; $rate = Flutterwave::transfers()->getRate( new GetRateRequest( sourceCurrency: 'NGN', destinationCurrency: 'GHS', amount: 10000, ) );
General Flow Transfer
For the general flow, use pre-created recipient and sender IDs:
use Gowelle\Flutterwave\Data\Transfer\CreateTransferRequest; // First, create recipient and sender (see above) $recipient = Flutterwave::transfers()->createRecipient(...); $sender = Flutterwave::transfers()->createSender(...); // Then create the transfer $transfer = Flutterwave::transfers()->create( new CreateTransferRequest( amount: 50000, sourceCurrency: 'NGN', destinationCurrency: 'NGN', recipientId: $recipient->id, senderId: $sender->id, reference: 'PAYOUT-' . uniqid(), ) );
Settlements
Retrieve settlement information (read-only).
Get Settlement
$settlement = Flutterwave::settlements()->get('settlement-id');
List Settlements
$settlements = Flutterwave::settlements()->list([ 'page' => 1, 'limit' => 20, ]);
Banks
Get bank information and resolve account details.
Get Banks by Country
$banks = Flutterwave::banks()->get('NG'); // Country code (e.g., NG, TZ, KE)
Get Bank Branches
$branches = Flutterwave::banks()->branches('bank-id');
Resolve Bank Account
$account = Flutterwave::banks()->resolveAccount( bankCode: '044', accountNumber: '0123456789', currency: 'NGN' ); // Access resolved account details echo $account->accountName; echo $account->accountNumber;
Mobile Networks
Get mobile money networks by country.
List Mobile Networks
$networks = Flutterwave::mobileNetworks()->list('TZ'); // Country code foreach ($networks as $network) { echo $network->name; echo $network->code; }
Charge Sessions
Charge Sessions provide database-backed tracking of direct charge transactions. This feature is particularly useful for tracking charges that require multiple authorization steps (PIN, OTP, redirects).
Features
- Automatic status updates via webhooks
- Event-driven session creation and updates
- Relationship tracking with User and Payment models
- Metadata storage for custom data
- Automatic cleanup of old sessions
Enabling Charge Sessions
- Publish and run the migration:
php artisan vendor:publish --tag="flutterwave-migrations"
php artisan migrate
- Configure in
config/flutterwave.php:
'charge_sessions' => [ 'enabled' => true, 'auto_create' => true, // Automatically create sessions on charge creation ],
Using Charge Sessions
Automatic Creation
When auto_create is enabled, sessions are automatically created when direct charges are created:
use Gowelle\Flutterwave\Facades\Flutterwave; use Gowelle\Flutterwave\Models\ChargeSession; $charge = Flutterwave::directCharge()->create([ 'amount' => 1000, 'currency' => 'TZS', 'reference' => 'ORDER-123', 'customer' => [...], 'payment_method' => [...], 'user_id' => auth()->id(), // Required for auto-create 'payment_id' => $payment->id, // Required for auto-create ]); // Session is automatically created and linked $session = ChargeSession::byRemoteChargeId($charge->id)->first();
Manual Creation
You can also create sessions manually:
use Gowelle\Flutterwave\Models\ChargeSession; $session = ChargeSession::create([ 'user_id' => auth()->id(), 'payment_id' => $payment->id, 'remote_charge_id' => $charge->id, 'status' => $charge->status->value, 'next_action_type' => $charge->nextAction->type->value ?? null, 'next_action_data' => $charge->nextAction->data ?? null, 'payment_method_type' => 'card', 'meta' => [ 'order_id' => '12345', ], ]);
Querying Sessions
use Gowelle\Flutterwave\Models\ChargeSession; use Gowelle\Flutterwave\Enums\DirectChargeStatus; // Find by remote charge ID $session = ChargeSession::byRemoteChargeId('charge-id')->first(); // Find pending sessions $pendingSessions = ChargeSession::pending()->get(); // Find completed sessions $completedSessions = ChargeSession::completed()->get(); // Find by status $succeededSessions = ChargeSession::withStatus(DirectChargeStatus::SUCCEEDED)->get(); // Access relationships $user = $session->user; $payment = $session->payment;
Updating Sessions
Sessions are automatically updated via webhooks when enabled is true. You can also update them manually:
$session->updateStatus(DirectChargeStatus::SUCCEEDED); $session->updateNextAction($nextActionData); $session->setMeta('custom_key', 'custom_value'); $session->save();
Session Cleanup
Run the cleanup command to remove old sessions:
php artisan flutterwave:cleanup-sessions
Or schedule it in your app/Console/Kernel.php:
protected function schedule(Schedule $schedule) { $schedule->command('flutterwave:cleanup-sessions')->daily(); }
Events & Listeners
The package dispatches Laravel events for important actions, allowing you to hook into the payment flow.
Available Events
FlutterwaveChargeCreated
Dispatched when a direct charge is created:
use Gowelle\Flutterwave\Events\FlutterwaveChargeCreated; Event::listen(FlutterwaveChargeCreated::class, function (FlutterwaveChargeCreated $event) { $chargeData = $event->chargeData; $requestData = $event->requestData; // Create charge session, send notification, etc. });
FlutterwaveChargeUpdated
Dispatched when charge authorization is submitted:
use Gowelle\Flutterwave\Events\FlutterwaveChargeUpdated; Event::listen(FlutterwaveChargeUpdated::class, function (FlutterwaveChargeUpdated $event) { $chargeData = $event->chargeData; $authorizationData = $event->authorizationData; // Update charge session, process completion, etc. });
FlutterwaveTransferCreated
Dispatched when a transfer is created (bank, mobile money, or wallet):
use Gowelle\Flutterwave\Events\FlutterwaveTransferCreated; Event::listen(FlutterwaveTransferCreated::class, function (FlutterwaveTransferCreated $event) { $transferData = $event->transferData; // Log transfer, update records, send notification, etc. logger()->info('Transfer created', [ 'id' => $transferData->id, 'status' => $transferData->status->value, 'amount' => $transferData->amount, ]); });
FlutterwaveWebhookReceived
Dispatched when a webhook is received and verified:
use Gowelle\Flutterwave\Events\FlutterwaveWebhookReceived; Event::listen(FlutterwaveWebhookReceived::class, function (FlutterwaveWebhookReceived $event) { $eventType = $event->getEventType(); // String (backward compatible) $eventTypeEnum = $event->getEventTypeEnum(); // WebhookEventType enum (recommended) $transactionData = $event->getTransactionData(); // Using helper methods on the event if ($event->isPaymentEvent()) { // Handle payment-related webhook } elseif ($event->isTransferEvent()) { // Handle transfer-related webhook } // Or using the enum directly if ($eventTypeEnum?->isPaymentEvent()) { // Handle payment-related webhook } elseif ($eventTypeEnum?->isTransferEvent()) { // Handle transfer-related webhook } if ($event->isSuccessful()) { // Transaction was successful } });
Built-in Listeners
The package includes built-in listeners that are automatically registered:
- CreateChargeSession - Creates charge sessions when
auto_createis enabled - UpdateChargeSession - Updates charge sessions when authorization is submitted
- UpdateChargeSessionFromWebhook - Updates charge sessions from webhook events
You can disable these by setting the appropriate configuration options.
Custom Event Listeners
Create your own event listeners:
// app/Listeners/ProcessSuccessfulPayment.php namespace App\Listeners; use Gowelle\Flutterwave\Events\FlutterwaveWebhookReceived; class ProcessSuccessfulPayment { public function handle(FlutterwaveWebhookReceived $event): void { if (!$event->isPaymentEvent() || !$event->isSuccessful()) { return; } $transactionData = $event->getTransactionData(); $chargeId = $transactionData['id'] ?? null; // Update your payment record, send confirmation email, etc. } }
Register in app/Providers/EventServiceProvider.php:
use App\Listeners\ProcessSuccessfulPayment; use Gowelle\Flutterwave\Events\FlutterwaveWebhookReceived; protected $listen = [ FlutterwaveWebhookReceived::class => [ ProcessSuccessfulPayment::class, ], ];
Webhooks
The package includes automatic webhook handling with signature verification and event dispatching.
Using the Built-in Webhook Route
The package automatically registers a webhook route at /webhooks/flutterwave (configurable via FLUTTERWAVE_WEBHOOK_PATH). This route:
- Verifies the webhook signature
- Dispatches the
FlutterwaveWebhookReceivedevent - Returns a 200 response
Configure the webhook URL in your Flutterwave dashboard to point to:
https://yourdomain.com/webhooks/flutterwave
Listening to Webhook Events
You can listen to webhook events and process them based on the event type. The package provides both string-based and enum-based methods for type safety.
Using String Event Types (Backward Compatible)
use Gowelle\Flutterwave\Events\FlutterwaveWebhookReceived; use Illuminate\Support\Facades\Event; Event::listen(FlutterwaveWebhookReceived::class, function (FlutterwaveWebhookReceived $event) { $payload = $event->payload; $eventType = $event->getEventType(); // Returns string $data = $event->getTransactionData(); // Process webhook event based on type match ($eventType) { 'charge.completed' => $this->handleChargeCompleted($data), 'charge.failed' => $this->handleChargeFailed($data), 'transfer.completed' => $this->handleTransferCompleted($data), default => logger()->info('Unhandled webhook event', ['type' => $eventType]), }; });
Using WebhookEventType Enum (Recommended)
For better type safety, use the WebhookEventType enum:
use Gowelle\Flutterwave\Enums\WebhookEventType; use Gowelle\Flutterwave\Events\FlutterwaveWebhookReceived; use Illuminate\Support\Facades\Event; Event::listen(FlutterwaveWebhookReceived::class, function (FlutterwaveWebhookReceived $event) { $eventTypeEnum = $event->getEventTypeEnum(); // Returns WebhookEventType enum $data = $event->getTransactionData(); if ($eventTypeEnum === null) { logger()->warning('Unknown webhook event type', ['payload' => $event->payload]); return; } // Use enum helper methods if ($eventTypeEnum->isPaymentEvent()) { // Handle payment-related webhook if ($eventTypeEnum->isSuccessful()) { $this->handleSuccessfulPayment($data); } else { $this->handleFailedPayment($data); } } elseif ($eventTypeEnum->isTransferEvent()) { // Handle transfer-related webhook $this->handleTransfer($data); } // Or use match with enum cases match ($eventTypeEnum) { WebhookEventType::CHARGE_COMPLETED => $this->handleChargeCompleted($data), WebhookEventType::CHARGE_FAILED => $this->handleChargeFailed($data), WebhookEventType::CHARGE_SUCCESSFUL => $this->handleChargeSuccessful($data), WebhookEventType::PAYMENT_COMPLETED => $this->handlePaymentCompleted($data), WebhookEventType::PAYMENT_FAILED => $this->handlePaymentFailed($data), WebhookEventType::PAYMENT_SUCCESSFUL => $this->handlePaymentSuccessful($data), WebhookEventType::TRANSFER_COMPLETED => $this->handleTransferCompleted($data), }; });
WebhookEventType Enum
The WebhookEventType enum provides type-safe webhook event handling with helper methods:
Available Event Types:
CHARGE_COMPLETED- Charge completed eventCHARGE_FAILED- Charge failed eventCHARGE_SUCCESSFUL- Charge successful eventPAYMENT_COMPLETED- Payment completed eventPAYMENT_FAILED- Payment failed eventPAYMENT_SUCCESSFUL- Payment successful eventTRANSFER_COMPLETED- Transfer completed event
Helper Methods:
fromString(?string $event): ?self- Convert string to enum (returns null for unknown types)isPaymentEvent(): bool- Check if event is payment-related (charge._ or payment._)isTransferEvent(): bool- Check if event is transfer-related (transfer.*)isChargeEvent(): bool- Check if event is charge-related (charge.*)isSuccessful(): bool- Check if event indicates success
Example Usage:
use Gowelle\Flutterwave\Enums\WebhookEventType; // Convert string to enum $enum = WebhookEventType::fromString('charge.completed'); if ($enum !== null) { // Type-safe event handling if ($enum->isPaymentEvent()) { // Handle payment event } } // Check event type if ($enum?->isSuccessful()) { // Event indicates success }
Manual Webhook Verification
If you need to verify webhooks manually (e.g., in a custom route):
use Gowelle\Flutterwave\Facades\Flutterwave; use Gowelle\Flutterwave\Exceptions\WebhookVerificationException; use Illuminate\Http\Request; Route::post('/custom-webhook', function (Request $request) { try { // Verify webhook signature Flutterwave::webhook()->verifyRequest($request); // Get event details (string) $eventType = Flutterwave::webhook()->getEventType($request); // Get event details (enum - recommended) $eventTypeEnum = Flutterwave::webhook()->getEventTypeEnum($request); $data = Flutterwave::webhook()->getEventData($request); // Process webhook // ... return response()->json(['status' => 'success']); } catch (WebhookVerificationException $e) { // Invalid webhook signature return response()->json(['error' => 'Invalid signature'], 401); } });
Note: Flutterwave sends the signature in the flutterwave-signature header, which is automatically handled by the verifyRequest method.
Error Handling
All API calls throw FlutterwaveException on error. The exception provides detailed information about the error:
use Gowelle\Flutterwave\Exceptions\FlutterwaveException; use Gowelle\Flutterwave\Facades\Flutterwave; try { $payment = Flutterwave::payments()->process($data); } catch (FlutterwaveException $e) { // Get user-friendly message $userMessage = $e->getUserFriendlyMessage(); // Check error type if ($e->isValidationError()) { // Handle validation error (400) logger()->warning('Validation error', ['message' => $userMessage]); } elseif ($e->isAuthenticationError()) { // Handle authentication error (401) logger()->error('Authentication failed', ['message' => $userMessage]); } elseif ($e->isRateLimitError()) { // Handle rate limit error (429) logger()->warning('Rate limit exceeded', ['message' => $userMessage]); } else { // Handle other API errors logger()->error('API error', ['message' => $userMessage]); } // Get technical details $errorData = $e->getErrorData(); logger()->error('Error details', [ 'message' => $errorData->getMessage(), 'code' => $errorData->getCode(), 'type' => $errorData->getType(), ]); }
Error Types
- ValidationError - Invalid request data (400)
- AuthenticationError - Invalid credentials (401)
- RateLimitError - Too many requests (429)
- ApiError - Other API errors (500, 502, 503, etc.)
Advanced Usage
Card Data Encryption
Critical: When making card charge requests, you must encrypt all card data (card number, CVV, expiry month, expiry year) using AES-256-GCM encryption before sending the request to Flutterwave.
Encryption Requirements
- Retrieve your encryption key from your Flutterwave dashboard under API Settings
- Use AES-256-GCM encryption with a 12-character nonce
- Base64 encode the encrypted data
- Include the nonce in your request
Encryption Process
- Generate a cryptographically secure random 12-character nonce (alphanumeric)
- Encrypt each card field (card number, CVV, expiry month, expiry year) using:
- Algorithm: AES-256-GCM
- Key: Your encryption key from Flutterwave dashboard (base64 decoded)
- IV/Nonce: The 12-character nonce
- Base64 encode the encrypted result
- Include both the nonce and encrypted fields in your request
Example Request Structure
'payment_method' => [ 'type' => 'card', 'card' => [ 'nonce' => 'RANDOMLY_GENERATED_12_CHAR_NONCE', 'encrypted_card_number' => 'BASE64_ENCRYPTED_CARD_NUMBER', 'encrypted_cvv' => 'BASE64_ENCRYPTED_CVV', 'encrypted_expiry_month' => 'BASE64_ENCRYPTED_EXPIRY_MONTH', 'encrypted_expiry_year' => 'BASE64_ENCRYPTED_EXPIRY_YEAR', ], ]
PHP Encryption Example
For PHP, you can use OpenSSL or libsodium. Here's a basic example using OpenSSL:
/** * Generate a cryptographically secure 12-character alphanumeric nonce */ function generateSecureNonce(int $length = 12): string { $characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; $charactersLength = strlen($characters); $nonce = ''; // Generate cryptographically secure random bytes $randomBytes = random_bytes($length); // Map bytes to alphanumeric characters for ($i = 0; $i < $length; $i++) { $nonce .= $characters[ord($randomBytes[$i]) % $charactersLength]; } return $nonce; } function encryptCardData(string $plainText, string $encryptionKey, string $nonce): string { $key = base64_decode($encryptionKey); $iv = $nonce; // 12-character nonce // Encrypt using AES-256-GCM $encrypted = openssl_encrypt( $plainText, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag ); // Combine encrypted data with authentication tag $encryptedWithTag = $encrypted . $tag; // Base64 encode return base64_encode($encryptedWithTag); } // Usage $encryptionKey = 'your_base64_encoded_encryption_key_from_dashboard'; $nonce = generateSecureNonce(12); // Generate cryptographically secure 12-character nonce $encryptedCardNumber = encryptCardData('5531886652142950', $encryptionKey, $nonce); $encryptedCvv = encryptCardData('564', $encryptionKey, $nonce); $encryptedExpiryMonth = encryptCardData('09', $encryptionKey, $nonce); $encryptedExpiryYear = encryptCardData('32', $encryptionKey, $nonce);
Reference: For detailed encryption documentation, examples, and best practices, see the Flutterwave Encryption Documentation.
Error Handling
If you send unencrypted or improperly encrypted card details, Flutterwave will return a 422 error:
{
"status": "failed",
"error": {
"type": "CLIENT_ENCRYPTION_ERROR",
"code": "11100",
"message": "Unable to decrypt encrypted fields provided",
"validation_errors": []
}
}
Idempotency Keys
Use idempotency keys to safely retry requests:
$charge = Flutterwave::directCharge()->create([ 'amount' => 1000, 'currency' => 'TZS', 'reference' => 'ORDER-123', 'idempotency_key' => 'unique-key-' . time(), // ... other data ]);
Trace IDs
Trace IDs help track requests across systems:
$charge = Flutterwave::directCharge()->create([ 'amount' => 1000, 'currency' => 'TZS', 'reference' => 'ORDER-123', 'trace_id' => 'trace-' . uniqid(), // ... other data ]);
Custom Retry Strategies
The package automatically retries on transient failures. You can customize retry behavior:
FLUTTERWAVE_MAX_RETRIES=5 FLUTTERWAVE_RETRY_DELAY=2000
Rate Limiting Customization
Adjust rate limiting based on your needs:
FLUTTERWAVE_RATE_LIMIT_ENABLED=true FLUTTERWAVE_RATE_LIMIT_MAX=200 FLUTTERWAVE_RATE_LIMIT_WINDOW=60
Direct Service Access
Access services directly without the facade:
use Gowelle\Flutterwave\Services\FlutterwaveDirectChargeService; $service = app(FlutterwaveDirectChargeService::class); $charge = $service->create($data);
Dependency Injection
Inject services into your classes:
use Gowelle\Flutterwave\Services\FlutterwaveDirectChargeService; class PaymentController { public function __construct( private FlutterwaveDirectChargeService $chargeService ) {} public function process() { $charge = $this->chargeService->create([...]); } }
Retry Logic
The package automatically retries failed requests with exponential backoff for:
- 5xx server errors
- 429 rate limit errors
- 408 timeout errors
- 503 service unavailable
Configuration
Configure retry behavior in .env:
FLUTTERWAVE_MAX_RETRIES=3 FLUTTERWAVE_RETRY_DELAY=1000 # milliseconds
The retry delay doubles with each attempt (exponential backoff).
Rate Limiting
Rate limiting prevents hitting Flutterwave API quotas. It's enabled by default and limits requests per time window.
Configuration
FLUTTERWAVE_RATE_LIMIT_ENABLED=true FLUTTERWAVE_RATE_LIMIT_MAX_REQUESTS=100 FLUTTERWAVE_RATE_LIMIT_PER_SECONDS=60
When the limit is reached, requests will wait until the window resets or throw a RateLimitException.
Testing
The package is fully testable using Laravel's HTTP faking capabilities.
Running Tests
vendor/bin/pest
Example Test
use Gowelle\Flutterwave\Facades\Flutterwave; use Illuminate\Support\Facades\Http; it('creates a direct charge successfully', function () { Http::fake([ 'api.flutterwave.com/*' => Http::response([ 'status' => 'success', 'data' => [ 'id' => 'charge-123', 'status' => 'pending', 'amount' => 1000, 'currency' => 'TZS', ], ], 200), ]); $charge = Flutterwave::directCharge()->create([ 'amount' => 1000, 'currency' => 'TZS', 'reference' => 'ORDER-123', 'customer' => [ 'email' => 'test@example.com', 'name' => 'Test User', ], 'payment_method' => [ 'type' => 'card', 'card' => [ 'number' => '5531886652142950', 'cvv' => '564', 'expiry_month' => '09', 'expiry_year' => '32', ], ], ]); expect($charge->id)->toBe('charge-123'); expect($charge->status->value)->toBe('pending'); });
Testing Webhooks
use Gowelle\Flutterwave\Events\FlutterwaveWebhookReceived; use Illuminate\Support\Facades\Event; it('handles webhook events', function () { Event::fake(); $payload = [ 'event' => 'charge.completed', 'data' => [ 'id' => 'charge-123', 'status' => 'successful', ], ]; event(new FlutterwaveWebhookReceived($payload)); Event::assertDispatched(FlutterwaveWebhookReceived::class); });
Troubleshooting
Common Issues
Authentication Errors
Problem: 401 Unauthorized errors
Solutions:
- Verify your
FLUTTERWAVE_CLIENT_IDandFLUTTERWAVE_CLIENT_SECRETare correct - Check that credentials match your environment (staging vs production)
- Ensure credentials haven't been rotated in Flutterwave dashboard
Webhook Verification Failures
Problem: Webhook signature verification fails
Solutions:
- Verify
FLUTTERWAVE_SECRET_HASHmatches your webhook secret in Flutterwave dashboard - Ensure the webhook route is accessible (not behind authentication)
- Check that the
flutterwave-signatureheader is being received
Rate Limit Errors
Problem: 429 Too Many Requests errors
Solutions:
- Increase
FLUTTERWAVE_RATE_LIMIT_MAX_REQUESTSif you have higher quotas - Implement request queuing for high-volume operations
- Use caching for frequently accessed data (banks, networks)
Charge Status Not Updating
Problem: Charge sessions not updating from webhooks
Solutions:
- Verify
charge_sessions.enabledistruein config - Check that webhook route is properly configured
- Ensure webhook events are being received (check logs)
- Verify database migrations have been run
Timeout Errors
Problem: Requests timing out
Solutions:
- Increase
FLUTTERWAVE_TIMEOUTvalue - Check network connectivity to Flutterwave API
- Verify firewall rules allow outbound connections
Debugging
Enable detailed logging:
FLUTTERWAVE_LOGGING_ENABLED=true FLUTTERWAVE_LOG_LEVEL=debug FLUTTERWAVE_LOG_REQUESTS=true FLUTTERWAVE_LOG_RESPONSES=true
Check logs in storage/logs/laravel.log for detailed API interactions.
Testing in Different Environments
Always test in staging before moving to production:
# Staging FLUTTERWAVE_ENVIRONMENT=staging FLUTTERWAVE_CLIENT_ID=your_staging_client_id FLUTTERWAVE_CLIENT_SECRET=your_staging_client_secret # Production FLUTTERWAVE_ENVIRONMENT=production FLUTTERWAVE_CLIENT_ID=your_production_client_id FLUTTERWAVE_CLIENT_SECRET=your_production_client_secret
Static Analysis
Run PHPStan for type checking:
vendor/bin/phpstan analyse --memory-limit=512M
For lower-resource systems, adjust the memory limit:
vendor/bin/phpstan analyse --memory-limit=256M
Code Style
Format code with Laravel Pint:
vendor/bin/pint
Contributing
Contributions are welcome! Please ensure:
- Tests pass:
vendor/bin/pest - Code style:
vendor/bin/pint - Type safety:
vendor/bin/phpstan analyse
License
MIT License. See LICENSE file for details.
Support
For issues and questions, please visit GitHub Issues.
Changelog
See CHANGELOG.md for detailed version history.