Integrating the Midtrans Payment Gateway in Laravel

Midtrans is one of the most popular payment gateways in Indonesia, supporting credit cards, bank transfers (virtual accounts), e-wallets like GoPay and ShopeePay, and QRIS. This tu...

Integrating the Midtrans Payment Gateway in Laravel

Midtrans is one of the most popular payment gateways in Indonesia, supporting credit cards, bank transfers (virtual accounts), e-wallets like GoPay and ShopeePay, and QRIS. This tutorial shows a Midtrans Snap integration in Laravel from scratch: installing the SDK, creating a payment token, showing the payment popup, then handling the notification webhook to update order status correctly.

Before diving into code, it helps to understand the big picture. Midtrans works with a server-to-server model: your application never touches the customer's card data or e-wallet balance directly. Instead, your server requests a token from Midtrans, the user completes payment on Midtrans's own interface, and Midtrans notifies your server via a webhook when the status changes. This separation makes your app far safer and reduces your PCI-DSS compliance burden, because sensitive data never passes through your database.

1. Installing the Midtrans SDK

Install the official Midtrans package via Composer:

composer require midtrans/midtrans-php

Register on the Midtrans dashboard, then grab your Server Key and Client Key from Settings → Access Keys. During development use sandbox mode.

2. Storing Configuration in .env

Never hard-code keys. Store them in .env:

MIDTRANS_SERVER_KEY=SB-Mid-server-xxxxxxxxxxxx
MIDTRANS_CLIENT_KEY=SB-Mid-client-xxxxxxxxxxxx
MIDTRANS_IS_PRODUCTION=false

Create a config/midtrans.php file to keep things tidy:

return [
    'server_key'    => env('MIDTRANS_SERVER_KEY'),
    'client_key'    => env('MIDTRANS_CLIENT_KEY'),
    'is_production' => env('MIDTRANS_IS_PRODUCTION', false),
];

By placing the keys in config, you can call config('midtrans.server_key') anywhere without touching env() directly. This matters because once you run php artisan config:cache in production, any env() call outside a config file returns null. Distinguishing the Server Key from the Client Key is also crucial: the Server Key is secret and used only on the backend to create tokens and verify webhooks, while the Client Key may appear in HTML because it only loads the payment interface.

3. Creating a Snap Token

Snap is Midtrans's ready-made payment interface. The flow: your server creates a Snap token, then the frontend uses it to open a payment popup. The advantage of Snap is that you don't need to build a payment page for each method yourself; Midtrans provides a complete UI that already supports virtual accounts, e-wallets, cards, and QRIS at once. Create a controller to process checkout:

use Midtrans\Config;
use Midtrans\Snap;

class PaymentController extends Controller
{
    public function __construct()
    {
        Config::$serverKey    = config('midtrans.server_key');
        Config::$isProduction = config('midtrans.is_production');
        Config::$isSanitized  = true;
        Config::$is3ds        = true;
    }

    public function pay(Order $order)
    {
        $params = [
            'transaction_details' => [
                'order_id'     => $order->code,
                'gross_amount' => (int) $order->total,
            ],
            'customer_details' => [
                'first_name' => $order->customer_name,
                'email'      => $order->customer_email,
            ],
        ];

        $snapToken = Snap::getSnapToken($params);

        return view('checkout', compact('order', 'snapToken'));
    }
}

Important: order_id must be unique for every transaction. If you reuse the same ID twice, Midtrans rejects it. Use a unique order code, not a bare auto-increment ID that could repeat on retry.

4. Showing the Payment Popup with snap.js

In the checkout view, load Midtrans's snap.js script and call snap.pay() with the token:

<script src="https://app.sandbox.midtrans.com/snap/snap.js"
        data-client-key="{{ config('midtrans.client_key') }}"></script>

<button id="pay-button">Pay Now</button>

<script>
document.getElementById('pay-button').onclick = function () {
    snap.pay('{{ $snapToken }}', {
        onSuccess: function (result) {
            window.location.href = '/order/success';
        },
        onPending: function (result) {
            window.location.href = '/order/pending';
        },
        onError: function (result) {
            alert('Payment failed.');
        }
    });
};
</script>

For production, change the script URL to https://app.midtrans.com/snap/snap.js.

5. Handling the Notification Webhook

The JavaScript callbacks above are for UX only; never trust them to change payment status. The source of truth is the server-to-server notification webhook from Midtrans. Create a POST endpoint to receive it:

use Midtrans\Notification;

public function notification(Request $request)
{
    Config::$serverKey    = config('midtrans.server_key');
    Config::$isProduction = config('midtrans.is_production');

    $notif = new Notification();

    $order = Order::where('code', $notif->order_id)->firstOrFail();
    $status = $notif->transaction_status;
    $fraud  = $notif->fraud_status;

    if ($status === 'capture' && $fraud === 'accept') {
        $order->update(['status' => 'paid']);
    } elseif ($status === 'settlement') {
        $order->update(['status' => 'paid']);
    } elseif (in_array($status, ['cancel', 'deny', 'expire'])) {
        $order->update(['status' => 'failed']);
    } elseif ($status === 'pending') {
        $order->update(['status' => 'pending']);
    }

    return response()->json(['message' => 'OK']);
}

Register the route and exclude it from CSRF because the request comes from Midtrans's servers, not the user's browser:

Route::post('/midtrans/notification', [PaymentController::class, 'notification']);

In Laravel 11, exclude this URI in bootstrap/app.php via validateCsrfTokens(except: ['midtrans/*']). Finally, register this webhook URL in the Midtrans dashboard under Settings → Configuration → Payment Notification URL.

6. Security Practices You Must Follow

  • Verify the signature: the Notification class validates automatically against your server key, so always use the official SDK rather than reading the raw payload.
  • Re-check the amount: compare the notification's gross_amount with the order total in your database before marking it paid.
  • Be idempotent: webhooks can be sent multiple times. Make status updates safe to repeat without duplicating side effects (e.g. send the email only once).
  • Always return HTTP 200: if you return an error, Midtrans keeps retrying the notification.

7. Testing and Debugging in Sandbox

When developing locally, Midtrans's webhook cannot reach localhost. Use a tunnel like Ngrok to expose your local server to the internet, then register that tunnel URL as the Payment Notification URL in the dashboard. To simulate payments, Midtrans provides test card numbers and status simulation buttons in the sandbox dashboard, so you can exercise the entire flow — success, pending, failed, and expired — without real money. Always log the incoming notification payload during development so you know exactly which fields Midtrans sends for each scenario.

You should also understand Midtrans's transaction statuses so your logic is correct. For card payments, the initial status is capture, which still needs its fraud_status checked. For other methods like virtual accounts or e-wallets, a paid state is marked settlement. A pending status means the user chose a method but hasn't paid yet, e.g. a VA number was issued but not transferred. Understanding this status map prevents the common mistake of marking an order paid too early.

With a Snap token from the server, a snap.js popup on the frontend, and the webhook as the source of truth, you have a secure, production-ready Midtrans integration. This pattern is identical for all payment methods because Snap unifies virtual accounts, e-wallets, and cards into one flow. Once this foundation works, you can extend it further by sending automatic receipt emails, showing a transaction history, or adding a re-payment feature for expired orders.

Yudhi
Written by
Yudhi
Founder & Lead Developer, GudangCode

Yudhi is the founder of GudangCode and a Laravel developer who has built dozens of ready-to-use business information systems — from POS and HRIS to management apps. He writes guides and articles on GudangCode to help Indonesian developers run, understand, and deploy Laravel source code correctly.

LaravelPHPMySQLSistem Informasi Bisnis See all articles by Yudhi
Want the full source code & apps?

Sign up free to download ready-to-use business applications, information systems, and Laravel source code.

Sign Up Free & Download
Laravel Midtrans Payment Gateway Pembayaran
Share this article
Back to Blog
📚 Free Learning Hub

Learn Coding for Free at DhieCoderWeb

Explore Laravel, PHP, JavaScript tutorials, source code, web development guides, and practical programming tips.

DhieCoderWeb
100+
Tutorials
Free
Learning
SEO
Tips
Visit Dhiecoderweb.com →

Get Full Access Now!

Join our membership and unlock exclusive access to all premium features. Fast, easy, and ready to use instantly.

Join Membership Now
Tim Support
Online
Isi data dulu untuk mulai chat:
Beri rating & testimoni sebelum menutup:
Live chat by gudangcode.com