Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Payfast Laravel Package

billowapp/payfast

Laravel package for integrating PayFast payments with a clean, expressive API. Create payment requests, redirect customers to PayFast, and handle ITN callbacks and payment status updates without boilerplate. Includes configurable credentials and sandbox support.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require billowapp/payfast
php artisan vendor:publish --provider="Billowapp\Payfast\PayfastServiceProvider"

This drops config/payfast.php into your app. Fill in:

// config/payfast.php (typical shape)
return [
    'merchant_id' => env('PAYFAST_MERCHANT_ID'),
    'merchant_key' => env('PAYFAST_MERCHANT_KEY'),
    'passphrase'  => env('PAYFAST_PASSPHRASE'),
    'sandbox'     => env('PAYFAST_SANDBOX', true),
    'return_url'  => env('PAYFAST_RETURN_URL'),
    'cancel_url'  => env('PAYFAST_CANCEL_URL'),
    'notify_url'  => env('PAYFAST_NOTIFY_URL'),
];

Where to look first (in this order)

  1. config/payfast.php — the single source of truth for credentials and URLs.
  2. The README's example — the fluent builder call sequence is the whole API surface.
  3. vendor/billowapp/payfast/src/PayfastServiceProvider.php — to confirm whether an ITN route is auto-registered, and what the facade alias is.
  4. The Payfast facade / service class — method names here are what you'll actually call day-to-day.

First use case

The canonical first integration is a one-off product checkout:

  1. User clicks "Pay" → you create an Order with status pending and a UUID m_payment_id.
  2. You build the PayFast form (merchant details, amount, item name, custom fields, signature) and either render it as an auto-submitting POST form or redirect the user to PayFast's process endpoint.
  3. PayFast POSTs an ITN to your notify_url → you verify and flip the order to paid.
  4. User lands back on return_url / cancel_url for a thank-you message — but fulfillment is never driven from this URL.

If you only get one thing working end-to-end, get the ITN handler working with the sandbox, because everything else is UI on top of that.

Implementation Patterns

Fluent builder → form fields → render

The package exposes a builder-style API on the Payfast facade. Representative shape (verify exact method names against the README of your locked version):

use Billowapp\Payfast\Facades\Payfast;

$fields = Payfast::setMerchantId(config('payfast.merchant_id'))
    ->setMerchantKey(config('payfast.merchant_key'))
    ->setReturnUrl(route('payfast.return', $order))
    ->setCancelUrl(route('payfast.cancel', $order))
    ->setNotifyUrl(route('payfast.itn'))
    ->setNameFirst($order->user->first_name)
    ->setNameLast($order->user->last_name)
    ->setEmailAddress($order->user->email)
    ->setMPaymentId($order->uuid)
    ->setAmount($order->total)          // ZAR, 2dp
    ->setItemName($order->description)
    ->setCustomStr1((string) $order->id)
    ->setSandbox(config('payfast.sandbox'))
    ->getFormFields();                   // includes the generated signature

Controller pattern — keep it thin

class PayfastCheckoutController
{
    public function redirect(Order $order)
    {
        abort_unless($order->isPending(), 403);

        return view('payfast.redirect', [
            'url'    => Payfast::getPaymentUrl(),
            'fields' => Payfast::buildForOrder($order),
        ]);
    }
}

Wrap the builder in an Action/Service (e.g. BuildPayfastPayment) so the order → gateway mapping lives in one testable place. The facade is stateful in a request, so avoid sprinkling Payfast::setAmount(...) calls across multiple controllers.

ITN webhook handling

Route::post('/payfast/itn', [PayfastItnController::class, 'handle'])
    ->withoutMiddleware([VerifyCsrfToken::class])
    ->name('payfast.itn');

public function handle(Request $request)
{
    if (! Payfast::isValidItn($request->all())) {   // or verify manually
        return response('', 400);
    }

    $order = Order::where('uuid', $request->input('m_payment_id'))->firstOrFail();

    // Never trust the amount from the payload — compare to the stored total
    if ((float) $request->input('amount_gross') < $order->total) {
        return response('', 400);
    }

    $order->markPaid($request->input('pf_payment_id'));

    return response('', 200);
}

Key rules baked into that snippet:

  • CSRF off for the ITN route (PayFast won't send a token).
  • Return exactly 200 OK with an empty body — anything else and PayFast retries.
  • Verify source IP against PayFast's published ranges (sandbox and live differ). If the package doesn't do this for you, add it in middleware.
  • Server-side amount check — the signed amount is what PayFast charged; compare it to your order, not to a client-supplied value.
  • Idempotency — ITNs can be replayed. Store pf_payment_id and short-circuit if the order is already paid.

State machine

Model payment states explicitly: pending → paid → cancelled/refunded. The ITN is the only event that should move an order to paid. The return_url should just look up the order and show a message — it must not do fulfillment, because the user can land there without the ITN having arrived yet.

Sandbox vs. live

Toggle via config, and use config('payfast.sandbox') rather than branching on app()->environment(). You'll want to test a live key in staging sometimes; tying sandbox to APP_ENV makes that awkward.

Custom fields as your metadata channel

custom_str1–custom_str4 round-trip through the ITN unchanged. Use custom_str1 for your internal order ID (short, numeric string) so the ITN handler doesn't have to trust m_payment_id alone — belt-and-braces against a bad client-side ID.

Gotchas and Tips

Stale package — check compatibility first

Last release was Feb 2020, so composer.json almost certainly pins laravel/framework to 5.x–7.x and PHP ^7.0. On Laravel 9/10/11 you'll either:

  • install with composer require billowapp/payfast --ignore-platform-reqs (may work since it's a thin wrapper), or
  • fork and bump illuminate/support, or
  • bypass the package entirely and just copy the signature-generation logic (it's ~15 lines).

Do a 10-minute spike before committing to the package on modern Laravel.

Signature is order- and encoding-sensitive

PayFast's signature is the MD5 of the URL-encoded parameter string in PayFast's defined field order, with the passphrase appended (also URL-encoded) if configured. Common failure modes:

  • Including empty/optional fields in the signature string.
  • Appending the passphrase when it's an empty string (breaks the hash).
  • Encoding the values after hashing vs. before — follow the README exactly; don't "fix" it.
  • Signature mismatch errors are opaque ("Signature does not match") — dump the concatenated string and diff character by character.

If the package's signature helper misbehaves in your setup, implement it yourself against the official PayFast docs; it's safer than fighting the wrapper.

notify_url must be publicly reachable

http://localhost/... will never receive an ITN. Use a tunnel (ngrok, expose, Laravel Valet's share) during development, or test against the sandbox directly on a staging domain. PayFast retries on failure, so a silently broken notify_url in production means orders stuck in pending.

Config caching bites

php artisan config:cache freezes PAYFAST_SANDBOX at cache time. If you toggle sandbox in .env and orders still hit production, run php artisan config:clear. Same trap for merchant keys when rotating credentials.

Amount formatting

PayFast expects a decimal string in ZAR ("149.99", no thousands separator, no currency symbol). Passing a float that stringifies to scientific notation (e.g. 1.5E-5) or an integer for 149.99 will fail validation. Cast with number_format($amount, 2, '.', '').

ITN verification checklist (do all four)

  1. Source IP in PayFast's CIDR list.
  2. Signature recomputed and matched.
  3. amount_gross (and item_name/m_payment_id) match the stored order.
  4. Optional but recommended: POST the notification back to PayFast's /eng/query/validate endpoint and confirm VALID. This catches spoofed ITNs that pass IP/signature checks.

The package historically covers #2 (and maybe #4); #1 and #3 are usually on you.

Return URL is for humans, ITN is for money

A user can complete payment and hit return_url before the ITN arrives (PayFast sends them nearly simultaneously, but not guaranteed). Your return_url page should show "Payment received, processing…" and poll or refresh order status — never mark the order paid there. Conversely, if the ITN arrives first, return_url should show a success state. Design for both orders.

cancel_url is not a cancellation event

Users can hit "cancel" on the PayFast page, close the tab, or abandon. Never treat cancel_url as "order failed" — leave the order pending and let a cron prune abandoned orders after N hours. You can't trust the user's action.

Sandbox vs. production keys

PayFast sandbox requires separate merchant credentials. The most common "it works in dev, fails in prod" bug is shipping sandbox keys to production (or the reverse) because someone forgot to flip PAYFAST_SANDBOX. Log the target URL on every checkout (sandbox.payfast.co.za vs www.payfast.co.za) in non-production environments for quick triage.

No refunds, no webhook headers

This package is payments-in only. Refunds need the PayFast dashboard or a direct API call — plan for that separately. And since PayFast doesn't sign a request header (the signature is inside the body), your ITN controller must be the one doing all verification; don't rely on a middleware that doesn't exist.

Testing

PayFast's sandbox won't generate an ITN for every test scenario. Write a FakePayfastItn helper that constructs a signed payload and POSTs it to your route in tests — this is the fastest way to cover paid, amount-mismatch, replayed, and invalid-signature branches without touching the network.

When to outgrow the package

If you find yourself patching the signature helper, adding IP verification, and re-implementing the field mapping, you're at the point where a 150-line custom PayfastGateway class (form builder + verifyItn()) is more maintainable than a stale 2020 wrapper. The package's value is the form field ordering and signature; once you've reproduced those, the dependency adds risk more than convenience.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core