Payment Integration

Flutterwave and Laravel: Payment Integration Guide

In brief

Integrate Flutterwave payments into your Laravel application. Full code examples for card, mobile money, bank transfer and USSD, with webhook handling and error recovery.

On this page 7 sections
Flutterwave and Laravel: Payment Integration Guide

Flutterwave is the most comprehensive payment gateway for African businesses: card payments, mobile money (M-Pesa, MTN, Airtel), bank transfers and USSD, all through one API. Here is how to integrate it properly into a Laravel application.

Why Flutterwave?

  • Pan-African coverage: 34 African countries, 150+ currencies.
  • Multiple payment methods: card, mobile money, bank transfer, USSD, Barter (Flutterwave's QR payment).
  • Reasonable fees: 2.9% for cards, 1.4% for mobile money, 0.5% for bank transfers.
  • Settlement in your local currency: receive UGX, KES, NGN or USD.
  • Decent developer experience: REST API with PHP SDK, webhooks and a testing sandbox.

Setup

composer require flutterwave/flutterwave-php

Add to .env:

FLW_PUBLIC_KEY=FLWPUBK-xxxxxxxxxxxxx
FLW_SECRET_KEY=FLWSECK-xxxxxxxxxxxxx
FLW_SECRET_HASH=my-secret-hash-for-webhooks
FLW_ENV=development  # or production

Add to config/services.php:

'flutterwave' => [
    'public_key' => env('FLW_PUBLIC_KEY'),
    'secret_key' => env('FLW_SECRET_KEY'),
    'secret_hash' => env('FLW_SECRET_HASH'),
    'env' => env('FLW_ENV', 'development'),
],

Initiating a payment

use Flutterwave\Flutterwave;

class PaymentController extends Controller
{
    public function initiatePayment(Order $order)
    {
        $reference = 'TFast-' . Str::random(12);

        // Store the reference locally
        $order->update(['payment_reference' => $reference, 'payment_status' => 'pending']);

        $data = [
            'tx_ref' => $reference,
            'amount' => $order->total,
            'currency' => 'UGX',
            'payment_options' => 'card,mobilemoney,ussd,banktransfer',
            'redirect_url' => route('payment.callback'),
            'customer' => [
                'email' => $order->customer->email,
                'name' => $order->customer->name,
                'phonenumber' => $order->customer->phone,
            ],
            'meta' => [
                'order_id' => $order->id,
            ],
            'customizations' => [
                'title' => config('company.name'),
                'description' => 'Payment for Order #' . $order->id,
                'logo' => asset('images/logo.png'),
            ],
        ];

        try {
            $payment = Flutterwave::createPayment($data);

            if ($payment['status'] === 'success') {
                return redirect($payment['data']['link']);
            }

            return back()->with('error', 'Payment initiation failed.');
        } catch (\Exception $e) {
            Log::error('Flutterwave payment error: ' . $e->getMessage());
            return back()->with('error', 'Payment service temporarily unavailable.');
        }
    }
}

Handling the callback

public function handleCallback(Request $request)
{
    $transactionId = $request->transaction_id;

    if (! $transactionId) {
        return redirect()->route('orders.index')->with('error', 'No transaction ID.');
    }

    try {
        $response = Flutterwave::verifyTransaction($transactionId);
        $data = $response['data'];

        $order = Order::where('payment_reference', $data['tx_ref'])->firstOrFail();

        if ($data['status'] === 'successful' && $data['amount'] == $order->total && $data['currency'] === 'UGX') {
            $order->markAsPaid($data['flw_ref'], $data['payment_type']);
        } else {
            $order->update(['payment_status' => 'failed']);
        }

        return redirect()->route('orders.show', $order);
    } catch (\Exception $e) {
        return redirect()->route('orders.index')->with('error', 'Payment verification failed.');
    }
}

Webhook handler (the real source of truth)

Never rely on the redirect callback alone, customers can close the browser before redirect. The webhook is your source of truth:

public function handleWebhook(Request $request): JsonResponse
{
    // Verify the webhook signature
    $signature = $request->header('verif-hash');
    $secretHash = config('services.flutterwave.secret_hash');

    if (! $signature || $signature !== $secretHash) {
        return response()->json(['status' => 'error'], 401);
    }

    $data = $request->input('data');

    if ($request->input('event') === 'charge.completed' && $data['status'] === 'successful') {
        $order = Order::where('payment_reference', $data['tx_ref'])->first();

        if ($order && $order->payment_status === 'pending') {
            $order->markAsPaid($data['flw_ref'], $data['payment_type']);
        }
    }

    return response()->json(['status' => 'ok']);
}

The webhook route must exclude CSRF protection

In bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: [
        'api/webhooks/flutterwave',
    ]);
})

Testing with the sandbox

Flutterwave provides test cards:

  • Success: 5531 8866 5214 2950, any future date, any CVV
  • Failure: use any other test card number
  • Mobile money: use test phone numbers provided in Flutterwave's sandbox docs

Always test the full flow (initiate payment, complete in sandbox, verify webhook received, verify order updated) before going live.

TFast Digital Agency integrates Flutterwave into Laravel applications with all payment methods, proper webhook handling, idempotency, error recovery and admin reconciliation dashboards, so your customers can pay however they prefer.

<!-- SEO keyword optimization: flutterwave payment integration laravel -->

Focusing on flutterwave payment integration laravel is essential for modern business success.