This guide explains how to integrate PayFast payment gateway with the Laravel Payments package.
PayFast is a South African payment gateway that supports multiple payment methods including:
PayFast integration uses direct API calls, so no additional package is required. Just ensure you have the Laravel Payments package installed and configured.
Add your PayFast credentials to your .env file:
PAYFAST_MERCHANT_ID=your_merchant_id_here
PAYFAST_MERCHANT_KEY=your_merchant_key_here
PAYFAST_PASSPHRASE=your_passphrase_here
PAYFAST_TEST_MODE=true
PAYFAST_WEBHOOK_URL=https://yoursite.com/payfast/webhook
PAYFAST_RETURN_URL=https://yoursite.com/payment/success
PAYFAST_CANCEL_URL=https://yoursite.com/payment/cancel
You can obtain these credentials from your PayFast merchant dashboard.
For development/testing:
PAYFAST_TEST_MODE=true
PAYFAST_MERCHANT_ID=10000100
PAYFAST_MERCHANT_KEY=testmerchantkey
For production:
PAYFAST_TEST_MODE=false
PAYFAST_MERCHANT_ID=your_production_merchant_id
PAYFAST_MERCHANT_KEY=your_production_merchant_key
You also need to add the configuration to your config/services.php:
'payfast' => [
'merchant_id' => env('PAYFAST_MERCHANT_ID'),
'merchant_key' => env('PAYFAST_MERCHANT_KEY'),
'passphrase' => env('PAYFAST_PASSPHRASE'),
'test_mode' => env('PAYFAST_TEST_MODE', true),
'webhook_url' => env('PAYFAST_WEBHOOK_URL'),
'return_url' => env('PAYFAST_RETURN_URL'),
'cancel_url' => env('PAYFAST_CANCEL_URL'),
],
use Mdiqbal\LaravelPayments\Facades\Payment;
$paymentRequest = [
'amount' => 299.99,
'currency' => 'ZAR', // PayFast only supports ZAR
'email' => 'customer@example.com',
'transaction_id' => 'TXN' . time(),
'redirect_url' => 'https://yoursite.com/payment/callback',
'customer' => [
'name' => 'John Smith',
'phone' => '27123456789'
],
'metadata' => [
'order_id' => 'ORD123456',
'user_id' => 789,
'custom_str1' => 'additional data',
'custom_int1' => 12345
]
];
$payment = Payment::gateway('payfast')->pay($paymentRequest);
This will return a payment URL that you need to redirect the user to, along with form data for posting:
if ($payment['success']) {
// Option 1: Create an auto-submitting form
return view('payment.payfast', [
'payment_url' => $payment['payment_url'],
'payment_data' => $payment['payment_data']
]);
// Option 2: Store payment ID and handle differently
session(['payfast_payment_id' => $payment['m_payment_id']]);
// Handle payment data as needed
}
Create a view at resources/views/payment/payfast.blade.php:
<!DOCTYPE html>
<html>
<head>
<title>Redirecting to Payment...</title>
</head>
<body onload="document.forms['payment_form'].submit()">
<form id="payment_form" method="POST" action="{{ $payment_url }}">
[@csrf](https://github.com/csrf)
[@foreach](https://github.com/foreach)($payment_data as $key => $value)
<input type="hidden" name="{{ $key }}" value="{{ $value }}">
[@endforeach](https://github.com/endforeach)
<p>Redirecting to secure payment page...</p>
<noscript>
<input type="submit" value="Continue to Payment">
</noscript>
</form>
</body>
</html>
// routes/web.php
Route::get('/payment/success', [PaymentController::class, 'success']);
Route::get('/payment/cancel', [PaymentController::class, 'cancel']);
Route::post('/payfast/webhook', [PayfastController::class, 'webhook']);
// app/Http/Controllers/PayfastController.php
use Mdiqbal\LaravelPayments\Facades\Payment;
class PayfastController extends Controller
{
public function webhook(Request $request)
{
// Parse webhook data
$gateway = Payment::gateway('payfast');
$webhookData = $gateway->parseCallback($request);
// Process the webhook
$result = $gateway->verify($webhookData);
if ($result['success']) {
$transactionId = $result['transaction_id'];
$status = $result['status'];
if ($status === 'completed') {
// Update order status
$order = Order::where('transaction_id', $transactionId)->first();
if ($order) {
$order->status = 'paid';
$order->paid_at = now();
$order->payment_method = $result['payment_method'];
$order->payfast_payment_id = $result['m_payment_id'];
$order->save();
}
}
}
// Always return a 200 OK response to acknowledge receipt
return response('OK', 200);
}
public function success(Request $request)
{
// User returned from PayFast after successful payment
// Note: Always rely on webhook for final status confirmation
return view('payment.success');
}
public function cancel(Request $request)
{
// User cancelled the payment
return view('payment.cancelled');
}
}
// First, get the payment ID (stored during initialization)
$mPaymentId = session('payfast_payment_id');
$verification = Payment::gateway('payfast')->verify($mPaymentId);
if ($verification['success']) {
$status = $verification['status'];
if ($status === 'completed') {
// Payment was successful
$mPaymentId = $verification['m_payment_id'];
$amount = $verification['amount'];
$currency = $verification['currency']; // Will always be ZAR
$paymentMethod = $verification['payment_method'];
}
}
$refundData = [
'm_payment_id' => 'PAYFAST_PAYMENT_ID',
'amount' => 150.00, // Optional - omit for full refund
'reason' => 'Customer requested refund',
'token' => 'PAYFAST_TOKEN' // Required for refunds
];
$refund = Payment::gateway('payfast')->refund($refundData);
$mPaymentId = 'CF_PAYMENT_ID';
$status = Payment::gateway('payfast')->getTransactionStatus($mPaymentId);
PayFast supports custom fields for additional data:
$paymentRequest = [
'amount' => 299.99,
'currency' => 'ZAR',
'email' => 'customer@example.com',
'transaction_id' => 'TXN' . time(),
'metadata' => [
'custom_str1' => 'User level', // Custom string field 1
'custom_str2' => 'Premium', // Custom string field 2
'custom_str3' => 'Monthly plan', // Custom string field 3
'custom_str4' => 'Promo code', // Custom string field 4
'custom_str5' => 'Reference', // Custom string field 5
'custom_int1' => 12345, // Custom integer field 1
'custom_int2' => 67890, // Custom integer field 2
'custom_int3' => 54321, // Custom integer field 3
'custom_int4' => 98765, // Custom integer field 4
'custom_int5' => 13579, // Custom integer field 5
],
'redirect_url' => 'https://yoursite.com/payment/callback'
];
$payment = Payment::gateway('payfast')->pay($paymentRequest);
$subscriptionData = [
'amount' => 99.99,
'email' => 'customer@example.com',
'description' => 'Monthly Premium Subscription',
'frequency' => '3', // 3 = Monthly, 2 = Weekly, 4 = Quarterly, 5 = Bi-annually, 6 = Annually
'cycles' => '12', // Number of billing cycles (0 = unlimited)
'return_url' => 'https://yoursite.com/subscription/success'
];
$subscription = Payment::gateway('payfast')->createSubscription($subscriptionData);
$mPaymentId = 'SUBSCRIPTION_PAYMENT_ID';
$result = Payment::gateway('payfast')->cancelSubscription($mPaymentId);
PayFast uses ITN (Instant Transaction Notification) webhooks to notify your application about payment status changes.
Configure your webhook URL in the PayFast merchant dashboard under "Settings" → "Instant Transaction Notification"
Create a route to handle webhooks:
// routes/web.php
Route::post('/payfast/webhook', [PayfastWebhookController::class, 'handleWebhook']);
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Mdiqbal\LaravelPayments\Facades\Payment;
class PayfastWebhookController extends Controller
{
public function handleWebhook(Request $request)
{
$gateway = Payment::gateway('payfast');
$webhookData = $gateway->parseCallback($request);
// Process the webhook
$result = $gateway->verify($webhookData);
if ($result['success']) {
$eventType = $result['event_type'];
$transactionId = $result['transaction_id'];
$mPaymentId = $result['m_payment_id'];
switch ($eventType) {
case 'payment.completed':
$this->handleSuccessfulPayment($result);
break;
case 'payment.pending':
$this->handlePendingPayment($result);
break;
case 'payment.failed':
$this->handleFailedPayment($result);
break;
case 'payment.cancelled':
$this->handleCancelledPayment($result);
break;
default:
$this->logInfo('Unknown webhook event: ' . $eventType, $result);
}
}
// Always return 200 OK to acknowledge receipt
return response('OK', 200);
}
protected function handleSuccessfulPayment($data)
{
// Update order status
$order = Order::where('transaction_id', $data['transaction_id'])->first();
if ($order) {
$order->status = 'paid';
$order->paid_at = now();
$order->payment_method = $data['payment_method'];
$order->payfast_payment_id = $data['m_payment_id'];
$order->merchant_info = array_merge($order->merchant_info ?? [], $data['merchant_info']);
$order->save();
// Send confirmation email
Mail::to($data['merchant_info']['email_address'])->send(new PaymentConfirmation($order));
}
}
protected function handlePendingPayment($data)
{
// Payment is being processed
$order = Order::where('transaction_id', $data['transaction_id'])->first();
if ($order) {
$order->status = 'processing';
$order->save();
}
}
protected function handleFailedPayment($data)
{
// Log failed payment
Log::warning('PayFast payment failed', [
'transaction_id' => $data['transaction_id'],
'm_payment_id' => $data['m_payment_id']
]);
// Update order status
$order = Order::where('transaction_id', $data['transaction_id'])->first();
if ($order) {
$order->status = 'failed';
$order->save();
}
}
protected function handleCancelledPayment($data)
{
// Handle cancelled payment
$order = Order::where('transaction_id', $data['transaction_id'])->first();
if ($order) {
$order->status = 'cancelled';
$order->save();
}
}
protected function logInfo($message, $data)
{
Log::info($message, $data);
}
}
pay() to generate payment data and signatureverify() to confirm payment statusPayFast uses MD5 signatures for webhook security:
The PayFast gateway provides detailed error messages:
$payment = Payment::gateway('payfast')->pay($paymentRequest);
if (!$payment['success']) {
$error = $payment['error'];
$message = $error['message'];
$code = $error['code'];
// Handle error based on type
if ($code === 'PAYMENT_FAILED') {
// Payment initialization failed
}
}
PAYMENT_FAILED - Payment initialization failedVERIFICATION_FAILED - Transaction verification failedREFUND_FAILED - Refund processing failedWEBHOOK_FAILED - Webhook processing failedINVALID_CURRENCY - Only ZAR is supportedINVALID_REQUEST - Invalid request parametersSIGNATURE_MISMATCH - Invalid signatureTOKEN_REQUIRED - Token required for refundUse test credentials for development:
PAYFAST_TEST_MODE=true
PayFast automatically presents appropriate payment methods based on:
PayFast processes payments in South African Rand only:
Note: If you need to accept international currencies, you'll need to handle currency conversion before sending to PayFast.
PayFast primarily serves South Africa:
PayFast implements reasonable rate limits:
pay() - Initialize a payment with signature generationverify($payload) - Verify ITN webhook payloadverify($mPaymentId) - Verify a transaction status via APIrefund() - Process a refund (full or partial)getTransactionStatus() - Get transaction statuscreateSubscription() - Create a recurring payment subscriptioncancelSubscription() - Cancel an active subscriptionparseCallback() - Parse ITN webhook parameters from requestgetSupportedCurrencies() - Get supported currenciesgetGatewayConfig() - Get gateway configurationgetPaymentMethodsForCountry() - Get payment methods for a country// Create a custom payment form
class PaymentController extends Controller
{
public function showPaymentForm(Request $request)
{
$paymentData = [
'merchant_id' => config('services.payfast.merchant_id'),
'merchant_key' => config('services.payfast.merchant_key'),
'return_url' => route('payment.success'),
'cancel_url' => route('payment.cancel'),
'notify_url' => route('payfast.webhook'),
'm_payment_id' => 'TXN' . time(),
'amount' => $request->input('amount', 100.00),
'item_name' => $request->input('description', 'Product Purchase'),
'email_address' => $request->input('email', Auth::user()->email),
];
// Generate signature
$gateway = Payment::gateway('payfast');
$signature = $gateway->generateSignature($paymentData);
$paymentData['signature'] = $signature;
return view('payment.form', compact('paymentData'));
}
}
// Handle multiple items in a single payment
$cartItems = Cart::all();
$totalAmount = $cartItems->sum('price');
$itemDescription = count($cartItems) . ' items in cart';
$paymentRequest = [
'amount' => $totalAmount,
'currency' => 'ZAR',
'email' => Auth::user()->email,
'transaction_id' => 'CART_' . time(),
'description' => $itemDescription,
'metadata' => [
'custom_str1' => 'Cart Purchase',
'custom_int1' => count($cartItems),
'cart_id' => Cart::current()->id
],
'redirect_url' => route('cart.success')
];
$payment = Payment::gateway('payfast')->pay($paymentRequest);
class SubscriptionController extends Controller
{
public function create(Request $request)
{
$subscriptionData = [
'amount' => 199.99,
'email' => $request->input('email'),
'description' => 'Monthly Premium Plan',
'frequency' => '3', // Monthly
'cycles' => '12', // 12 months
'return_url' => route('subscription.success'),
'metadata' => [
'plan_id' => 'premium_monthly',
'user_id' => Auth::id()
]
];
$subscription = Payment::gateway('payfast')->createSubscription($subscriptionData);
if ($subscription['success']) {
// Store subscription details
Subscription::create([
'user_id' => Auth::id(),
'plan_id' => 'premium_monthly',
'amount' => 199.99,
'frequency' => 'monthly',
'status' => 'pending',
'payfast_m_payment_id' => $subscription['m_payment_id']
]);
return view('payment.subscription', [
'payment_data' => $subscription['payment_data'],
'payment_url' => $subscription['payment_url']
]);
}
return back()->with('error', 'Failed to create subscription');
}
}
class PayfastWebhookHandler
{
public function handleAdvancedWebhook(Request $request)
{
$gateway = Payment::gateway('payfast');
$data = $gateway->parseCallback($request);
// Verify signature
if (!$this->verifySignature($data)) {
Log::error('Invalid PayFast webhook signature', $data);
return response('Invalid signature', 400);
}
// Get merchant details
$merchant = Merchant::where('payfast_merchant_id', $data['merchant_id'])->first();
if (!$merchant) {
Log::error('Unknown PayFast merchant ID: ' . $data['merchant_id']);
return response('Unknown merchant', 400);
}
// Process based on payment status
switch ($data['payment_status']) {
case 'COMPLETE':
$this->processCompletePayment($data, $merchant);
break;
case 'PENDING':
$this->processPendingPayment($data, $merchant);
break;
case 'FAILED':
$this->processFailedPayment($data, $merchant);
break;
}
ret...
How can I help you explore Laravel packages today?