Laravel
Payment Gateway
Integrate Midtrans payment gateway into Laravel using the official PHP SDK. Covers installation, configuration, creating transaction tokens, Snap popup, webhook handling, and checking transaction status.
Install Midtrans PHP SDK: Use Composer to install the official
midtrans/midtrans-php package.terminal
BASH
composer require midtrans/midtrans-php
Environment Config: Add your Midtrans credentials to
.env. Get keys from the Midtrans dashboard under Settings → Access Keys..env
ENV
MIDTRANS_SERVER_KEY=SB-Mid-server-xxxxxxxxxxxxxxxxxxxx
MIDTRANS_CLIENT_KEY=SB-Mid-client-xxxxxxxxxxxxxxxxxxxx
MIDTRANS_IS_PRODUCTION=false
MIDTRANS_IS_SANITIZED=true
MIDTRANS_IS_3DS=true
Config File: Create
config/midtrans.php to expose the environment variables through Laravel's config system.config/midtrans.php
PHP
<?php
return [
'server_key' => env('MIDTRANS_SERVER_KEY'),
'client_key' => env('MIDTRANS_CLIENT_KEY'),
'is_production' => env('MIDTRANS_IS_PRODUCTION', false),
'is_sanitized' => env('MIDTRANS_IS_SANITIZED', true),
'is_3ds' => env('MIDTRANS_IS_3DS', true),
];
Create Transaction Token: In your controller, configure Midtrans and call
Snap::getSnapToken() to generate a payment token sent to the frontend.app/Http/Controllers/PaymentController.php
PHP
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Midtrans\Config;
use Midtrans\Snap;
use App\Models\Order;
class PaymentController extends Controller
{
public function __construct()
{
Config::$serverKey = config('midtrans.server_key');
Config::$isProduction = config('midtrans.is_production');
Config::$isSanitized = config('midtrans.is_sanitized');
Config::$is3ds = config('midtrans.is_3ds');
}
public function createToken(Request $request)
{
$order = Order::findOrFail($request->order_id);
$params = [
'transaction_details' => [
'order_id' => $order->id,
'gross_amount' => $order->total_price,
],
'customer_details' => [
'first_name' => $order->user->name,
'email' => $order->user->email,
'phone' => $order->user->phone ?? '-',
],
'item_details' => $order->items->map(fn($item) => [
'id' => $item->product_id,
'price' => $item->price,
'quantity' => $item->qty,
'name' => $item->product->name,
])->toArray(),
];
$snapToken = Snap::getSnapToken($params);
return response()->json(['snap_token' => $snapToken]);
}
}
Snap Popup JS Integration: Include the Midtrans Snap JS script and call
snap.pay() with the token. Use sandbox URL for development.resources/views/checkout.blade.php
HTML
<script src="https://app.sandbox.midtrans.com/snap/snap.js"
data-client-key="{{ config('midtrans.client_key') }}"></script>
<button id="pay-btn" class="btn btn-primary">Pay Now</button>
<script>
document.getElementById('pay-btn').addEventListener('click', function () {
fetch('/payment/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ order_id: {{ $order->id }} })
})
.then(res => res.json())
.then(data => {
snap.pay(data.snap_token, {
onSuccess: function (result) {
console.log('Payment success:', result);
window.location.href = '/payment/success';
},
onPending: function (result) {
console.log('Payment pending:', result);
window.location.href = '/payment/pending';
},
onError: function (result) {
console.error('Payment error:', result);
alert('Payment failed. Please try again.');
},
onClose: function () {
alert('You closed the payment window without completing the payment.');
}
});
});
});
</script>
Webhook / Notification Handler: Midtrans sends a POST request to your notification URL. Verify the signature hash, then update the order status accordingly. Register this route without CSRF.
app/Http/Controllers/PaymentController.php
PHP
public function handleNotification(Request $request)
{
$notification = new \Midtrans\Notification();
$transactionStatus = $notification->transaction_status;
$fraudStatus = $notification->fraud_status;
$orderId = $notification->order_id;
$order = Order::findOrFail($orderId);
if ($transactionStatus === 'capture') {
if ($fraudStatus === 'challenge') {
$order->update(['payment_status' => 'challenge']);
} elseif ($fraudStatus === 'accept') {
$order->update(['payment_status' => 'paid']);
}
} elseif ($transactionStatus === 'settlement') {
$order->update(['payment_status' => 'paid']);
} elseif (in_array($transactionStatus, ['cancel', 'deny', 'expire'])) {
$order->update(['payment_status' => 'failed']);
} elseif ($transactionStatus === 'pending') {
$order->update(['payment_status' => 'pending']);
}
return response()->json(['status' => 'ok']);
}
Exclude Webhook from CSRF: Add the notification route to the CSRF exception list in
bootstrap/app.php (Laravel 11) or VerifyCsrfToken.php (Laravel 10).app/Http/Middleware/VerifyCsrfToken.php
PHP
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
protected $except = [
'payment/notification',
];
}
Check Transaction Status: Use
Transaction::status() to programmatically query the current status of an order from Midtrans API.app/Http/Controllers/PaymentController.php
PHP
use Midtrans\Transaction;
public function checkStatus($orderId)
{
$status = Transaction::status($orderId);
return response()->json([
'order_id' => $status->order_id,
'transaction_status' => $status->transaction_status,
'payment_type' => $status->payment_type,
'gross_amount' => $status->gross_amount,
'fraud_status' => $status->fraud_status ?? null,
]);
}
Register Routes: Add payment routes to
routes/web.php. The notification route should be in api.php or excluded from CSRF.routes/web.php
PHP
use App\Http\Controllers\PaymentController;
Route::middleware('auth')->group(function () {
Route::post('/payment/token', [PaymentController::class, 'createToken']);
Route::get('/payment/status/{order}', [PaymentController::class, 'checkStatus']);
Route::get('/payment/success', fn() => view('payment.success'));
Route::get('/payment/pending', fn() => view('payment.pending'));
});
// Webhook – no auth, no CSRF
Route::post('/payment/notification', [PaymentController::class, 'handleNotification']);