Bonaventure OgetoBy Bonaventure Ogeto|

Accept Payments with Paystack in Laravel

To accept Paystack payments in Laravel, create a PaymentController that uses the Laravel HTTP client to initialize a transaction with the Paystack API. Redirect the user to Paystack checkout. In the callback route, verify the transaction using your secret key and mark the order as paid.

Setup

Add your Paystack keys to .env:

PAYSTACK_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxx
PAYSTACK_PUBLIC_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxx

Add them to config/services.php:

// config/services.php
'paystack' => [
    'secret_key' => env('PAYSTACK_SECRET_KEY'),
    'public_key' => env('PAYSTACK_PUBLIC_KEY'),
],

Order Model and Migration

php artisan make:model Order -m
// database/migrations/create_orders_table.php
Schema::create('orders', function (Blueprint $table) {
    $table->id();
    $table->string('reference')->unique();
    $table->string('email');
    $table->integer('amount'); // in kobo
    $table->string('currency', 3)->default('NGN');
    $table->boolean('paid')->default(false);
    $table->timestamp('paid_at')->nullable();
    $table->timestamps();
});

Payment Controller

// app/Http/Controllers/PaymentController.php
namespace App\Http\Controllers;

use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

class PaymentController extends Controller
{
    public function initialize(Request $request)
    {
        $request->validate([
            'email' => 'required|email',
            'amount' => 'required|numeric|min:1',
        ]);

        $reference = 'order_' . Str::random(12);

        $order = Order::create([
            'reference' => $reference,
            'email' => $request->email,
            'amount' => (int) ($request->amount * 100),
        ]);

        $response = Http::withToken(config('services.paystack.secret_key'))
            ->post('https://api.paystack.co/transaction/initialize', [
                'email' => $request->email,
                'amount' => $order->amount,
                'reference' => $reference,
                'callback_url' => route('payment.callback'),
            ]);

        $data = $response->json();

        if ($data['status'] ?? false) {
            return redirect($data['data']['authorization_url']);
        }

        return back()->with('error', $data['message'] ?? 'Failed');
    }

    public function callback(Request $request)
    {
        $reference = $request->query('reference') ?? $request->query('trxref');

        if (!$reference) {
            return redirect('/')->with('error', 'Missing reference');
        }

        $response = Http::withToken(config('services.paystack.secret_key'))
            ->get('https://api.paystack.co/transaction/verify/' . $reference);

        $data = $response->json();

        if (($data['status'] ?? false) && $data['data']['status'] === 'success') {
            $order = Order::where('reference', $reference)->first();

            if ($order && $data['data']['amount'] === $order->amount) {
                Order::where('reference', $reference)
                    ->where('paid', false)
                    ->update(['paid' => true, 'paid_at' => now()]);

                return view('payment.success', ['order' => $order]);
            }
        }

        return view('payment.failed');
    }
}

Routes

// routes/web.php
use App\Http\Controllers\PaymentController;

Route::post('/pay', [PaymentController::class, 'initialize'])->name('payment.initialize');
Route::get('/payment/callback', [PaymentController::class, 'callback'])->name('payment.callback');

CSRF Exemption for Webhooks

Paystack webhook requests will not have Laravel CSRF tokens. Exempt the webhook route:

// app/Http/Middleware/VerifyCsrfToken.php
protected $except = [
    'webhooks/paystack',
];

The callback route is a GET request, so CSRF does not apply to it.

JSON API for SPA Frontends

// For API routes (routes/api.php)
Route::post('/pay', function (Request $request) {
    $reference = 'order_' . Str::random(12);

    $response = Http::withToken(config('services.paystack.secret_key'))
        ->post('https://api.paystack.co/transaction/initialize', [
            'email' => $request->email,
            'amount' => (int) ($request->amount * 100),
            'reference' => $reference,
        ]);

    $data = $response->json();

    if ($data['status'] ?? false) {
        return response()->json([
            'authorization_url' => $data['data']['authorization_url'],
            'access_code' => $data['data']['access_code'],
            'reference' => $data['data']['reference'],
        ]);
    }

    return response()->json(['error' => $data['message'] ?? 'Failed'], 400);
});

Production Checklist

  1. Set up webhooks. See Handle Paystack Webhooks in Laravel.
  2. Use HTTPS. Required for callback and webhook URLs.
  3. Validate amounts server-side. Look up prices from your database.
  4. Switch keys. Use sk_live_ in production .env.
  5. Prevent double-granting. The where('paid', false) condition handles this.

Ship Payments Faster

Key Takeaways

  • Laravel HTTP client (Http::withToken()) makes Paystack API calls clean and readable.
  • Store your Paystack keys in .env and access them with config() or env().
  • Amounts are in kobo (NGN), pesewas (GHS), or cents. Multiply by 100 before sending to Paystack.
  • Exempt callback and webhook routes from CSRF verification in VerifyCsrfToken middleware.
  • Always verify the transaction server-side. The redirect alone is not proof of payment.
  • No external packages needed. Laravel HTTP client handles all the Paystack REST API calls.

Frequently Asked Questions

Do I need a Paystack Laravel package?
No. Laravel HTTP client handles all the REST API calls cleanly. Community packages exist but add unnecessary abstraction. The Http::withToken() syntax is already concise.
Can I use Laravel Cashier with Paystack?
Laravel Cashier is built for Stripe and Paddle. It does not support Paystack. Build your Paystack integration directly using the HTTP client and Paystack REST API.
How do I handle multiple currencies in Laravel?
Pass the currency parameter when initializing. Store the expected currency in your Order model and verify it matches during payment verification.
Should I use Laravel Jobs for payment processing?
For webhook processing, yes. Dispatch a job from your webhook controller so the response returns immediately. For the callback route, inline processing is fine since the user is waiting for a response.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon