Require the package
Laravel finds the service provider and the Flutterwave facade on its own.
composer require abraham-flutterwave/laravel-payment
Take card, bank transfer, USSD and mobile money payments in your Laravel app, then send payouts and react to payment events. This guide takes you from composer require to a verified test payment and a safe launch, in about 30 minutes.
This guide is for the developer adding Flutterwave to a Laravel app. Have these ready and setup goes straight through. Tick them off as you go.
Composer pulls it from Packagist. Pick the tab that matches your app: most apps want the first one.
For any Laravel app using Blade, an API or Livewire. Updates arrive with composer update.
Laravel finds the service provider and the Flutterwave facade on its own.
composer require abraham-flutterwave/laravel-payment
It publishes config/flutterwave.php, offers to add the four Flutterwave keys to .env, and prints the webhook URL you'll need in step two.
php artisan flutterwave:install
The installer publishes only the config. Add --billable to save customers' cards, or publish the example checkout routes to start from working code:
php artisan vendor:publish --tag=flutterwave-routes
Publishing every tag at once puts all three frontends' components in one folder and adds routes and a migration you may not want. Publish by tag instead.
For Inertia apps with React, Vue 3 or Svelte 5. You get a pay button and a bank transfer component, styled like Flutterwave's own screens.
composer require abraham-flutterwave/laravel-payment
Pick one framework. The components are copied into resources/js/components/flutterwave, so they're yours to edit and add no npm dependency.
php artisan flutterwave:install --react # or --vue, or --svelte
Run your usual Vite build so the components, stylesheet and fonts are bundled. Skip this and the imports won't resolve.
npm run build
This is where the app learns which account to charge and how to recognise Flutterwave's payment notifications. Keep your editor and the Flutterwave dashboard open side by side.
On the dashboard, open the API keys page. Copy the public, secret and encryption keys into the lines the installer added:
FLW_PUBLIC_KEY=FLWPUBK_TEST-xxxxxxxx-X FLW_SECRET_KEY=FLWSECK_TEST-xxxxxxxx-X FLW_ENCRYPTION_KEY=xxxxxxxxxxxx
Test keys contain _TEST. Mixing a test key with a live one is the most common setup fault, and it shows up as an authorisation error at checkout, not as a config error.
Invent a long random string and treat it like a password. Put it in .env, then type the same value into Settings → Webhooks → Secret hash on the dashboard. It's how your app proves a payment notification really came from Flutterwave.
FLW_SECRET_HASH=choose-a-long-random-string
Leave it empty and the package rejects every webhook, on purpose.
On the same dashboard screen, paste your app's webhook URL. The package registers this route for you; the installer printed the exact address for your app.
https://your-app.com/flutterwave/webhook
Replace your-app.com with your real domain. If you changed FLW_WEBHOOK_PATH, use that path instead.
If your app caches config, Laravel won't see the new .env values until you clear it.
php artisan config:clear
It checks your keys, webhook secret hash and URL, cache, queue and admin routes, then asks Flutterwave to confirm your secret key. Fix anything marked as failing before you move on.
php artisan flutterwave:doctor
APP_URL/flutterwave/payment/callback.FLW_DEFAULT_COUNTRY to match.Every option, with its default, is in config/flutterwave.php and on the documentation site's config reference.
Do this once before launch. It takes a few minutes and catches key mismatches, webhook faults and currency problems in one pass.
Both keys in .env should contain _TEST. flutterwave:doctor says which mode it sees.
The quickest way is from the terminal. It creates a hosted payment link and opens it in your browser:
php artisan flutterwave:pay you@example.com 100 --open
Or from your own code, which is what your customers will use:
use Flutterwave\Payments\Facades\Flutterwave;
return Flutterwave::redirect([
'amount' => 5000,
'email' => auth()->user()->email,
'tx_ref' => $order->reference, // so you can match it later
]);Use a card from Flutterwave's testing documentation. It covers success, failure and OTP flows. Real cards are always declined with test keys.
Fulfil orders from the webhook event, not the redirect: customers close tabs, and bank transfers can finish minutes later. The package has already checked the signature and re-fetched the charge from the API when this runs.
use Flutterwave\Payments\Events\ChargeCompleted;
use Illuminate\Support\Facades\Event;
// In AppServiceProvider::boot()
Event::listen(function (ChargeCompleted $event) {
if (! $event->isSuccessful()) {
return;
}
$order = Order::firstWhere('reference', $event->reference());
if ($order
&& $event->amount() >= $order->amount
&& $event->currency() === $order->currency) {
$order->markAsPaid();
}
});Flutterwave can't reach localhost. To send a signed webhook for your test payment to your local app, use the transaction id from the dashboard:
php artisan flutterwave:ping-webhook 4975363
A healthy test shows the same transaction in each:
php artisan flutterwave:verify <transaction id> reports it as successfulstorage/logs/flutterwave.log shows the charge.completed webhook arrivingYour keys, webhook URL and secret hash are all correct. You're ready to go live.
Switching to real money is a handful of changes on your production server. Work through them in order.
Search by what you're seeing. Each entry explains what's actually happening before telling you what to change. Run php artisan flutterwave:doctor first: it catches most of these on its own.
What's happening
Flutterwave's checkout refuses to load on a bare IP address, and the inline modal can't start without your public key.
Try in this order
localhost or a domain, not 127.0.0.1.FLW_PUBLIC_KEY is set, then run php artisan config:clear.npm run build.What's happening
The secret key Flutterwave received doesn't belong with the request. Almost always a test key mixed with a live one, or a stale value from cached config.
Try in this order
php artisan flutterwave:doctor. It flags test and live keys mixed together.php artisan config:clear (or config:cache in production) after editing .env.What's happening
Your app never handled the webhook. It didn't arrive, was rejected, or arrived with nothing listening for ChargeCompleted.
Try in this order
storage/logs/flutterwave.log. No charge.completed line means the webhook never got through.https:// and the path.ChargeCompleted listener, and that it matches orders by tx_ref.FLW_WEBHOOK_QUEUE? Make sure a queue worker is running for that queue.php artisan flutterwave:ping-webhook <id>.Confirm the payment with php artisan flutterwave:verify <id> and mark the order paid by hand, so the customer isn't left waiting. Then flutterwave:resend-webhook <id> once it's fixed.
What's happening
The signature check failed. The secret hash in your .env doesn't match the one on the dashboard, or it's empty, which rejects everything by design.
Try in this order
FLW_SECRET_HASH in both places rather than trusting a copy.flutterwave-signature or verif-hash header.What's happening
Before firing ChargeCompleted, the package re-fetches the charge from the API so a forged payload can't mark an order paid. That lookup failed, so it answered 500 and Flutterwave will retry.
Try in this order
storage/logs/flutterwave.log for the API's error message.What's happening
Flutterwave retries webhooks until it gets a 2xx. The package remembers handled webhooks in your cache, but a cache that forgets between requests, or isn't shared between servers, can't do that.
Try in this order
FLW_WEBHOOK_DEDUPE_STORE=redis (or database). The array store forgets everything.php artisan flutterwave:doctor; it tests that the cache keeps values and can lock.What's happening
With test keys, Flutterwave leaves transfers pending unless the reference says how they should end. Sandbox transfers also need your server's IP whitelisted on the dashboard.
Try in this order
'test_outcome' => 'success' (or 'failure') when you create the transfer.What's happening
These screens refund customers and move money, so every action checks the flutterwave.manage ability. Until you define it, everyone is refused, even signed-in admins.
Try in this order
AppServiceProvider::boot(): Gate::define('flutterwave.manage', fn (User $user) => $user->is_admin);Route::flutterwaveAdmin() sits inside your authenticated route group.No entry matches that. Try a different word, or report it. An unmatched problem is worth telling us about.
Pick the channel that matches what you need. Code and setup questions go to the developer channels; questions about money that has already moved go through your Flutterwave dashboard.
The Flutterwave developer community. Good for setup questions, error messages and "is this normal?"
When you're stuck mid-setup Email developers@flutterwavego.comThe developer experience team. Include your package version, Laravel and PHP versions, and what you've already tried.
For detail that needs attachments Reference Flutterwave error referenceWhat each API error message means. The package passes Flutterwave's message through in FlutterwaveException.
If the package itself is at fault, an issue gets it in front of the engineers who maintain it.
For reproducible faultsOpen an issue on the package's GitHub issues. Search first: if someone has hit it already, add to their thread rather than opening a new one. Questions belong in Slack, not issues.
Issues are public. Redact keys, customer names, emails and card details from logs before you post. If you've already exposed a key, roll it on the dashboard immediately.
Copy this, fill it in, and paste it into the issue:
### Description What went wrong, in one or two sentences. ### Steps to reproduce 1. 2. 3. **Expected:** what should have happened **Actual:** what happened instead **Happens:** every time / sometimes / once ### Configuration - Package version (composer show abraham-flutterwave/laravel-payment): - Laravel version: - PHP version: - Frontend: Blade / Livewire / Inertia (React, Vue, Svelte) / API - Environment: test keys / live keys - Output of php artisan flutterwave:doctor --offline: ### Evidence Error message, stack trace, or lines from storage/logs/flutterwave.log, with keys and customer details removed.
A good report answers "what did you expect, what happened, and how do I make it happen on my machine?" A failing test is the best possible version of that.