laravel-payfast maintained by tumelo
Laravel PayFast
A modern, feature-complete Laravel package for PayFast payment gateway integration. Built with PHP 8.3+ and supports Laravel 11/12.
Features
- Redirect Payments - Standard PayFast hosted payment page
- Onsite Payments - Embedded payment engine (no redirect)
- ITN Webhooks - Automatic signature & IP validation with Laravel events
- Subscriptions - Standard recurring and ad hoc (tokenization) billing
- Refunds - Full and partial refund processing
- Transaction History - Date range queries with pagination
- Split Payments - Automatic fund splitting between accounts
- SPA Support - JSON API endpoints with Vue 3 and React components
- Blade Components - Drop-in payment form components
- Sandbox Support - Toggle between test and production environments
Requirements
- PHP 8.3+
- Laravel 11 or 12
Installation
composer require tumelo/laravel-payfast
Publish the configuration file:
php artisan vendor:publish --tag=payfast-config
Add your PayFast credentials to .env:
PAYFAST_MERCHANT_ID=your_merchant_id
PAYFAST_MERCHANT_KEY=your_merchant_key
PAYFAST_PASSPHRASE=your_passphrase
PAYFAST_TESTING=true
PAYFAST_RETURN_URL=https://yourapp.com/payment/success
PAYFAST_CANCEL_URL=https://yourapp.com/payment/cancel
PAYFAST_NOTIFY_URL=https://yourapp.com/payfast/webhook
Quick Start
Simple Payment (Redirect)
use Tumelo\PayFast\Facades\PayFast;
use Tumelo\PayFast\Data\PaymentData;
use Tumelo\PayFast\Data\CustomerData;
// In your controller
public function checkout()
{
$formData = PayFast::buildFormData(new PaymentData(
merchantPaymentId: 'ORDER-001',
amount: 199.99,
itemName: 'Premium Widget',
customer: new CustomerData(
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com',
),
));
return view('payment.redirect', [
'formData' => $formData,
'processUrl' => PayFast::getProcessUrl(),
]);
}
Onsite Payment (Embedded)
$result = PayFast::onsite(new PaymentData(
merchantPaymentId: 'ORDER-002',
amount: 99.99,
itemName: 'Basic Widget',
));
// Returns: ['uuid' => '...', 'form_data' => [...], 'process_url' => '...']
Blade Component
{{-- Redirect payment form --}}
<x-payfast::payment-form :data="$paymentData" />
{{-- With auto-submit (redirect immediately) --}}
<x-payfast::payment-form :data="$paymentData" :auto-submit="true" />
{{-- Custom submit button text --}}
<x-payfast::payment-form :data="$paymentData" submit-text="Complete Purchase" />
{{-- Onsite (embedded) payment --}}
<x-payfast::payment-form :data="$paymentData" :onsite="true" />
Subscriptions
Create a Subscription
use Tumelo\PayFast\Data\SubscriptionData;
use Tumelo\PayFast\Enums\SubscriptionType;
use Tumelo\PayFast\Enums\BillingFrequency;
$formData = PayFast::buildSubscriptionFormData(new SubscriptionData(
merchantPaymentId: 'SUB-001',
amount: 49.99,
itemName: 'Monthly Plan',
subscriptionType: SubscriptionType::STANDARD,
frequency: BillingFrequency::MONTHLY,
cycles: 0, // indefinite
customer: new CustomerData(
firstName: 'Jane',
lastName: 'Smith',
email: 'jane@example.com',
),
));
Manage Subscriptions
// Fetch subscription details
$details = PayFast::subscriptions()->fetch('subscription-token');
// Pause for 2 cycles
PayFast::subscriptions()->pause('subscription-token', 2);
// Resume
PayFast::subscriptions()->unpause('subscription-token');
// Cancel
PayFast::subscriptions()->cancel('subscription-token');
// Update
PayFast::subscriptions()->update('subscription-token', [
'cycles' => 6,
'amount' => 5999,
]);
// Ad hoc charge (tokenized billing)
PayFast::subscriptions()->adhocCharge(
token: 'subscription-token',
amount: 149.99,
itemName: 'One-time upgrade',
);
Refunds
use Tumelo\PayFast\Data\RefundData;
$result = PayFast::refund(new RefundData(
paymentId: 'pf_123456',
amount: 50.00,
reason: 'Customer request',
));
if ($result->isSuccessful()) {
// Refund processed
}
Transaction History
// Fetch by date range
$transactions = PayFast::transactionHistory(
from: now()->subMonth(),
to: now(),
);
// Convenience methods
$today = PayFast::transactions()->daily(now());
$thisWeek = PayFast::transactions()->weekly(now());
$thisMonth = PayFast::transactions()->monthly(now());
// Fetch single transaction
$transaction = PayFast::fetchTransaction('pf_payment_id');
Webhook Events
The package automatically handles ITN (Instant Transaction Notification) callbacks and dispatches Laravel events. Register listeners in your app:
// In EventServiceProvider or using Event::listen
use Tumelo\PayFast\Events\PaymentCompleted;
use Tumelo\PayFast\Events\PaymentFailed;
use Tumelo\PayFast\Events\PaymentPending;
use Tumelo\PayFast\Events\SubscriptionCreated;
use Tumelo\PayFast\Events\SubscriptionCancelled;
use Tumelo\PayFast\Events\RefundProcessed;
Event::listen(PaymentCompleted::class, function (PaymentCompleted $event) {
$event->merchantPaymentId; // Your order ID
$event->payFastPaymentId; // PayFast's payment ID
$event->transaction->amountGross; // Total amount
$event->transaction->amountNet; // After fees
$event->transaction->customer; // CustomerData object
$event->raw; // Raw ITN array
});
Event::listen(SubscriptionCreated::class, function (SubscriptionCreated $event) {
$event->token; // Subscription token for future API calls
$event->transaction; // TransactionResult
});
Webhook Route
The ITN webhook endpoint is automatically registered at /payfast/webhook (configurable in config/payfast.php). It includes:
- IP validation - Ensures requests come from PayFast servers
- Signature validation - Verifies request integrity
- Merchant ID validation - Confirms the request is for your account
SPA Integration (Vue 3 / React)
Enable API Endpoints
In your .env:
PAYFAST_SPA_ENABLED=true
Publish Frontend Components
# Vue 3
php artisan vendor:publish --tag=payfast-vue
# React
php artisan vendor:publish --tag=payfast-react
Vue 3 Usage
<template>
<PayFastPayment
:amount="199.99"
item-name="Premium Widget"
payment-id="ORDER-001"
:customer="{ firstName: 'John', lastName: 'Doe', email: 'john@example.com' }"
@success="handleSuccess"
@cancelled="handleCancelled"
@error="handleError"
/>
</template>
<script setup>
import { PayFastPayment } from './components/payfast/PayFastPayment.vue'
function handleSuccess(data) { /* ... */ }
function handleCancelled() { /* ... */ }
function handleError(error) { /* ... */ }
</script>
Using the composable directly:
import { usePayFast } from './components/payfast/usePayFast'
const { initiatePayment, loading, error, result, submitRedirectForm } = usePayFast()
async function pay() {
const response = await initiatePayment({
merchant_payment_id: 'ORDER-001',
amount: 99.99,
item_name: 'Widget',
})
if (response?.type === 'redirect') {
submitRedirectForm(response.process_url, response.form_data)
}
}
React Usage
import { PayFastPayment } from './components/payfast/PayFastPayment'
function CheckoutPage() {
return (
<PayFastPayment
amount={199.99}
itemName="Premium Widget"
paymentId="ORDER-001"
customer={{ firstName: 'John', lastName: 'Doe', email: 'john@example.com' }}
onSuccess={(data) => console.log('Success:', data)}
onCancelled={() => console.log('Cancelled')}
onError={(error) => console.error(error)}
/>
)
}
Using the hook directly:
import { usePayFast } from './components/payfast/usePayFast'
function CheckoutPage() {
const { initiatePayment, loading, error, submitRedirectForm } = usePayFast()
async function handleCheckout() {
const response = await initiatePayment({
merchant_payment_id: 'ORDER-001',
amount: 99.99,
item_name: 'Widget',
})
if (response?.type === 'redirect') {
submitRedirectForm(response.process_url!, response.form_data!)
}
}
return <button onClick={handleCheckout} disabled={loading}>Pay Now</button>
}
API Endpoints
When SPA mode is enabled, these JSON endpoints are available:
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/payfast/initiate |
Create payment (redirect or onsite) |
| POST | /api/payfast/subscription |
Create subscription |
| GET | /api/payfast/transaction/{id} |
Fetch transaction status |
| POST | /api/payfast/refund |
Process refund |
| GET | /api/payfast/history |
Fetch transaction history |
Custom Fields
Pass custom data through PayFast for retrieval in ITN callbacks:
$payment = new PaymentData(
merchantPaymentId: 'ORDER-001',
amount: 100.00,
itemName: 'Widget',
customStrings: [
'custom_str1' => 'order-reference',
'custom_str2' => 'user-metadata',
],
customIntegers: [
'custom_int1' => 42,
],
);
Access them in your event listener:
Event::listen(PaymentCompleted::class, function ($event) {
$ref = $event->transaction->customStrings['custom_str1']; // 'order-reference'
$meta = $event->transaction->customIntegers['custom_int1']; // 42
});
Signature Generation
Generate and verify PayFast signatures:
$signature = PayFast::generateSignature($data);
$isValid = PayFast::isValidSignature($data, $signature);
Configuration
Full configuration options in config/payfast.php:
return [
'merchant_id' => env('PAYFAST_MERCHANT_ID'),
'merchant_key' => env('PAYFAST_MERCHANT_KEY'),
'passphrase' => env('PAYFAST_PASSPHRASE'),
'testing' => env('PAYFAST_TESTING', true),
'return_url' => env('PAYFAST_RETURN_URL'),
'cancel_url' => env('PAYFAST_CANCEL_URL'),
'notify_url' => env('PAYFAST_NOTIFY_URL'),
'urls' => [
'sandbox' => 'https://sandbox.payfast.co.za',
'production' => 'https://www.payfast.co.za',
],
'itn' => [
'validate_ip' => true,
'allowed_ips' => ['197.97.145.144/28', '41.74.179.192/27'],
'validate_signature' => true,
],
'webhook_route' => '/payfast/webhook',
'spa' => [
'enabled' => env('PAYFAST_SPA_ENABLED', false),
'prefix' => 'api/payfast',
'middleware' => ['api', 'auth:sanctum'],
],
];
Testing
The package includes a full test suite:
composer test
Or run specific suites:
vendor/bin/phpunit --testsuite=Unit
vendor/bin/phpunit --testsuite=Feature
Sandbox Testing
- Create a sandbox account at sandbox.payfast.co.za
- Set
PAYFAST_TESTING=truein your.env - Use sandbox credentials for
PAYFAST_MERCHANT_IDandPAYFAST_MERCHANT_KEY - All payments will use the sandbox environment
Publishing Assets
# Config only
php artisan vendor:publish --tag=payfast-config
# Blade views (for customization)
php artisan vendor:publish --tag=payfast-views
# Vue 3 components
php artisan vendor:publish --tag=payfast-vue
# React components
php artisan vendor:publish --tag=payfast-react
License
MIT License. See LICENSE for details.