opsofts / laravel-flutterwave
A minimal Flutterwave integration for Laravel, built directly on Laravel's own HTTP client.
Requires
- php: ^8.2
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
README
A minimal Flutterwave v4 integration for Laravel, built directly on Laravel's own HTTP client (Illuminate\Support\Facades\Http). No external HTTP SDK dependency — just illuminate/support/illuminate/http, which ship with every Laravel app and are maintained on the same cadence as the framework itself.
Targets the Flutterwave v4 API, currently in public beta. v4 changed a lot from v3 — OAuth 2.0 authentication, a customer/payment-method/charge flow instead of one-shot checkout, and a new webhook signature scheme. See "Coming from v3" below if you're upgrading.
What it does
Implements the operations most apps need to collect and manage payments on v4:
- Create a customer —
POST /customers - Create a payment method —
POST /payment-methods(mobile money, OPay, bank transfer, USSD, or a pre-encrypted card) - Initiate a charge —
POST /chargesagainst a customer + payment method - Authorize a charge —
PUT /charges/{id}, for charges that come back needing a PIN, OTP, or address verification - Verify a charge —
GET /charges/{id} - Refund a charge —
POST /refunds, full or partial - Verify a webhook signature — HMAC-SHA256 the raw body with your secret hash and compare against the
flutterwave-signatureheader
OAuth token fetching and caching (10-minute tokens, refreshed automatically) is handled for you. Anything beyond the above (transfers, subaccounts, settlements, virtual accounts, etc.) isn't included — the package stays intentionally small. Feel free to extend src/Flutterwave.php if you need more of the API surface.
Requirements
- PHP 8.2+
- Laravel 11, 12, or 13
Install
composer require opsofts/laravel-flutterwave php artisan vendor:publish --tag=flutterwave-config
Set your credentials in .env. These are v4's Client ID / Client Secret, not the old v3 Public Key / Secret Key — if your dashboard still shows Public Key / Private Key / Encryption Key, click "Switch to v4 live API keys" first.
FLUTTERWAVE_CLIENT_ID=...
FLUTTERWAVE_CLIENT_SECRET=...
FLUTTERWAVE_SECRET_HASH=your-chosen-webhook-secret
FLUTTERWAVE_ENVIRONMENT=sandbox
FLUTTERWAVE_SECRET_HASH is a value you choose and enter under Settings > Webhooks in your dashboard — Flutterwave doesn't issue it.
FLUTTERWAVE_ENVIRONMENT selects the base URL (developersandbox-api.flutterwave.com for sandbox, f4bexperience.flutterwave.com for production, per Flutterwave's docs). v4 is a public beta and its infrastructure has been known to move — if requests start failing against production, check your dashboard/docs for the current base URL and set FLUTTERWAVE_BASE_URL to override it directly.
Usage
use Opsofts\LaravelFlutterwave\Facades\Flutterwave; // 1. Create a customer $customer = Flutterwave::createCustomer([ 'email' => $user->email, 'name' => ['first' => $user->first_name, 'last' => $user->last_name], ]); $customerId = $customer['data']['id']; // 2. Create a payment method (mobile money shown here -- no encryption needed). // For cards, encrypt the card number/expiry/CVV client-side first; see // "What this package deliberately does not do" below. $paymentMethod = Flutterwave::createPaymentMethod([ 'type' => 'mobile_money', 'mobile_money' => [ 'country_code' => '234', 'network' => 'MTN', 'phone_number' => '9012345678', ], ]); $paymentMethodId = $paymentMethod['data']['id']; // 3. Initiate the charge $charge = Flutterwave::initiateCharge([ 'reference' => $orderReference, 'currency' => 'NGN', 'customer_id' => $customerId, 'payment_method_id' => $paymentMethodId, 'redirect_url' => route('payment.callback'), 'amount' => $amount, ]); $chargeId = $charge['data']['id']; // 4. Handle data.next_action from step 3: // - redirect_url -> redirect the payer to $charge['data']['next_action']['redirect_url']['url'] // - payment_instruction -> show $charge['data']['next_action']['payment_instruction']['note'] // - requires_pin / requires_otp / requires_additional_fields -> collect input, then: $result = Flutterwave::authorizeCharge($chargeId, [ 'authorization' => [ 'type' => 'otp', 'otp' => ['code' => $otpFromCustomer], ], ]); // 5. Verify before giving value -- don't trust a webhook or redirect alone $charge = Flutterwave::verifyTransaction($chargeId); if ($charge['data']['status'] === 'succeeded') { // mark the order/payment as paid } // Refund (full or partial) Flutterwave::refund($chargeId, amount: $partialAmount, reason: 'Customer requested refund'); // Webhook signature verification -- do this before trusting ANY webhook payload. // Pass the RAW request body, not the parsed array. $isValid = Flutterwave::verifyWebhookSignature( rawPayload: $request->getContent(), signatureHeader: $request->header('flutterwave-signature'), ); if (! $isValid) { abort(401); }
Coming from v3 / this package's earlier version
If you integrated against v3 (or an earlier version of this package), note what changed:
- Auth: static Secret Key → OAuth 2.0
client_credentialsgrant (Client ID + Client Secret, 10-minute access tokens). This package fetches and caches the token for you; you never handle it directly. - Initializing a payment: v3's single
POST /v3/paymentscall (returning a hosted-checkoutauthorization_url) is replaced by a customer → payment-method → charge flow. There's no v4 hosted-checkout equivalent yet — Flutterwave has said one is coming, but it wasn't available at the time of writing.initiateCharge()plus handlingnext_actionis the closest v4 equivalent. - Webhook header:
verif-hash(a plain value compare) →flutterwave-signature(an HMAC-SHA256 of the raw body, base64-encoded).verifyWebhookSignature()now takes the raw payload as well as the header. - Refund shape: the transaction reference moved from the URL into the request body as
charge_id, alongside a requiredreason.
What this package deliberately does not do
No card encryption. v4 requires card numbers, expiry, and CVV to be AES-256-GCM encrypted before they reach your server, using a nonce and encryption key obtained per Flutterwave's encryption guide — this happens client-side (in JS), not on your backend. This package accepts already-encrypted card fields in createPaymentMethod() but does not perform the encryption itself; doing that server-side would mean plaintext card data touching your server, which is exactly what this design avoids.
No card storage, no saved payment methods beyond what Flutterwave's payment_method_id already represents. This package only ever talks to Flutterwave's API and hands back what Flutterwave returns — persisting IDs/references on your own models is the calling application's responsibility, same as with any SDK.
Testing
Http::fake() works exactly as it does for any Laravel HTTP client usage — fake the idp.flutterwave.com/* pattern for the OAuth token call and your configured base URL for API calls, rather than hitting the real API. No custom test helpers are provided; standard Laravel HTTP testing covers this package fully.
License
MIT.