Hai. Tulisan ini sebenarnya merupakan dokumentasi pribadi saya, mengingat saya cukup banyak menggunakan Midtrans sebagai payment gateway pada proyek-proyek yang kerjakan, baik yang menggunakan Laravel, Node JS, maupun Spring Boot (baru-baru ini).
Midtrans, yang sekarang sudah jadi milik Gojek, merupakan payment gateway favorit saya untuk menerima pembayaran dan saya selalu menyarankan ke klien untuk menggunakan Midtrans dibandingkan payment gateway lain. Bukan hanya karena saya sudah familiar, tapi kemudahan yang diberikan, pilihan channel pembayaran, biaya yang masuk akal, stabil, simulator pembayaran, dan yang paling penting: penarikan uang yang cepat.
Sejauh ini, ada dua metode integrasi yang sudah saya gunakan, yaitu SNAP API dan Core API, tergantung permintaan klien. Apa bedanya? Jika menggunakan SNAP API, kita akan menampilkan interface yang diberikan Midtrans seperti pemilihan metode pembayaran, detail pembelian hingga nomor rekening pembayaran di website kita. Tampilannya seperti ini:
Kelebihannya, integrasi SNAP API sangat mudah dan cepat. Kekurangannya, tampilannya cuma itu-itu saja dan sama di semua website. Kita memang bisa mengubah font, warna hingga logo, tapi tampilan dasarnya tetap sama. Orang akan mudah tau jika itu menggunakan Midtrans. Sebenarnya ini tidak bisa disebut sebagai kekurangan…
Jika ingin tampilan yang unik dengan desain sendiri, tidak ingin ada yang tau payment gateway apa yang kita gunakan, kita bisa menggunakan CORE API. Dengan CORE API, kita bisa membuat alur pembayaran sendiri, hingga desain unik yang tidak akan sama dengan website lain. Tapi implementasi CORE API cukup rumit.
Di posting ini, saya akan menulis mengenai integrasi Midtrans ke Laravel 11 menggunakan SNAP API. Sebenarnya bisa digunakan di Laravel 10, 9, 8, 7 bahkan 6 dengan kode yang hampir sama. Saya pertama mengintegrasikan Midtrans saat Laravel masih versi 6, dan ternyata kodenya masih sama saja dengan versi 11.
Saya akan coba menjelaskan teori, implementasi kode, dan repository yang bisa kamu clone. Disini, saya berasumsi bahwa kamu sudah mengetahui basic Laravel seperti route, model, controller, view, konfigurasi, hingga middleware.
Konsep yang harus dipahami
Pertama, saya ingin menjelaskan konsep yang perlu kita pahami. Dengan memahami konsep ini, integrasi akan menjadi lebih mudah, bahkan jika menggunakan teknologi lain seperti Node JS atau Java sekalipun, karena konsepnya sama.
1. Snap Token
Semua website tentu memiliki unique identifier untuk data-data yang dimiliki. Dalam kasus e-commerce, identifier ini dapat berupa order id, uuid, atau auto generated primary key. Nah, demikian juga Midtrans. Untuk menggunakan SNAP API, butuh sebuah unique identifier yang disebut snap token. Snap token merupakan token pembayaran yang berbeda untuk setiap order. Misalnya, customer Sindi melakukan pembelian lipstik dan parfum, kemudian website kita memberikan id 202411081235ABCD untuk order tersebut, maka, order 202411081235ABCD akan memiliki snap token tersendiri, begitu juga order-order lain.
Jadi, snap token merupakan identifier pembayaran untuk suatu order tertentu.
2. Pembuatan SNAP Token
Pertama, saya anggap bahwa website kamu sudah memiliki sistem order dan sudah dapat mencatat data order seperti total harga, item, ongkos kirim, dan lain sebagainya. Jika sudah, pada saat customer pertama kali akan melakukan pembayaran, back-end kita mengirimkan data-data order seperti total harga, data customer, item pembelian dan data lain ke Midtrans melalui API. Kemudian Midtrans akan memberikan balasan berupa snap token.
Untuk apa tokennya? Pada tabel orders di database, kamu wajib membuat kolom bernama “snap_token” (bisa juga membuat tabel khusus dengan relasi one to many). Awalnya snap_token ini masih null, namun kemudian akan diupdate dengan token yang diberikan Midtrans.
Jadi, untuk membuat snap token, sistem kita perlu mengirimkan data-data yang diperlukan ke Midtrans, kemudian Midtrans akan memberikan snap token untuk digunakan sebagai identifier.
3. Alur pembayaran
Setelah mendapatkan snap token, kita perlu menggunakan token tersebut pada kode Javascript yang digunakan untuk menampilkan interface seperti pada gambar 1. Dengan token ini, interface tersebut akan dapat menampilkan data customer, data order dan total harga sesuai dengan data yang kita kirimkan saat membuat snap token.
Kemudian, customer dapat memilih salah satu channel pembayaran yang tersedia, misalnya GoPay, transfer bank, virtual account hingga pembayaran melalui Indomaret. Anggap saja customer memilih membayar menggunakan virtual account, dan sudah mendapatkan kode pembayaran, customer akan melakukan pembayaran misalnya melalui aplikasi mobile banking.
4. Callback
Setelah customer berhasil melakukan pembayaran, Midtrans akan mengirim notifikasi ke website kita yang disebut dengan callback. Callback ini berisi data-data pembayaran, dan yang paling penting adalah status pembayaran, seperti berhasil, batal, expire, fraud, dan sebagainya. Nah, kita perlu menangani status ini. Misalnya jika berhasil, kita perlu memperbarui status order menjadi misalnya “sudah dibayar” dan melanjutkan proses selanjutnya, jika expire, kita perlu memperbarui status order menjadi misalnya “kadaluarsa” dan mengulang proses pembayaran.
Dalam proses ini, kita perlu menyiapkan endpoint khusus, misalnya: /payment/midtrans-callback
bebas apa endpointnya dan harus bisa diakses publik (tidak ada otorisasi).
Implementasi
Pertama, silahkan buat akun di Midtrans. Disini, kita akan menggunakan mode sandbox selama tahap pengembangan dan uji coba.
Supaya tidak terlalu panjang, saya hanya akan menjelaskan mulai dari pembuatan tabel orders
hingga pembayaran saja. Untuk ENV, file konfigurasi dan pengecualian CSRF, silahkan lihat di repository berikut. Jangan lupa berikan star ya, terima kasih!
https://github.com/mulyosyahidin/laravel-midtrans
Access Key
Pertama, kita perlu mendapatkan access key. Ada dua access key, yaitu client key dan server key. Server key akan digunakan untuk berkomunikasi di sisi back-end seperti proses pembuatan token, sedangkan client key digunakan disisi front-end.
Untuk mendapatkan access key, silahkan buka Pengaturan > Access Key seperti pada gambar 2.
Silahkan catat merchant id, client key dan server key yang diberikan.
Membuat Tabel Orders
Singkatnya, tabel order
ini berisi data-data order seperti id, order id, dan total harga. Untuk item order, akan disimpan di tabel order_items
, sedangkan data pembayaran Midtrans akan disimpan di tabel payments
dengan relasi one to many dari tabel orders
, ini untuk mengantisipasi jika pembayaran kadaluarsa, maka kita bisa membuat snap token baru tanpa menghapus token yang lama.
Membuat tabel orders
Di tabel orders
saya hanya membuat data basic saja. Silahkan sesuaikan dengan kebutuhan kamu, misalnya menambahkan kolom user_id
untuk menandai siapa yang melakukan order.
php artisan make:model Order -m
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->string('order_id', 32)->unique();
$table->double('total_price', 8, 2);
$table->enum('status', ['pending', 'processing', 'completed', 'cancelled'])->default('pending');
$table->enum('payment_status', ['unpaid', 'paid', 'failed'])->default('unpaid');
$table->timestamps();
});
Membuat tabel order_items
Tabel order_items
akan menyimpan item-item order. Umumnya data ini berasal dari tabel products
, namun disini saya akan membuatnya statis seperti nama, harga dan kuantitasnya. Silahkan sesuaikan dengan kebutuhan kamu.
php artisan make:model Order_item -m
Schema::create('order_items', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(\App\Models\Order::class, 'order_id')->nullable()->constrained()->cascadeOnDelete();
$table->string('product_name');
$table->double('price', 8, 2);
$table->integer('quantity');
$table->timestamps();
});
Membuat tabel payments
Sebenarnya, kolom snap_token bisa saja disimpan di tabel orders
, namun terkadang saya memiliki kebutuhan untuk menyimpan log pembayaran customer yang akan digunakan untuk kebutuhan lain seperti analisa perilaku customer hingga profiling.
php artisan make:model Payment -m
Schema::create('payments', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(\App\Models\Order::class, 'order_id')->nullable()->constrained()->cascadeOnDelete();
$table->string('snap_token')->nullable();
$table->string('status', 32)->default('pending');
$table->dateTime('expired_at')->nullable();
$table->dateTime('paid_at')->nullable();
$table->timestamps();
});
Untuk melihat model Order, Order_item dan Payment, silahkan buka repository Github nya.
Membuat data dummy
Untuk keperluan uji coba, saya membuat data dummy berisi data-data order. Karena kodenya cukup panjang, silahkan buka pada tautan berikut: DummyOrderSeeder.php
Midtrans Service
Midtrans Service merupakan utilitas untuk mengakses API Midtrans. Pertama, install dependency yang dibutuhkan:
composer require midtrans/midtrans-php
Midtrans Service berisi fungsi-fungsi seperti pembuatan token, validasi signature key, mengambil data order dari callback dan penentuan status. Buat filenya di: app/Services/MidtransService.php
dan isi dengan kode berikut: MidtransService.php
Membuat Controller Order
Controller ini akan berada di folder app\Http\Controllers\Customer
.
php artisan make:controller Customer/OrderController
class OrderController extends Controller
{
public function index()
{
$orders = Order::all();
return view('customer.orders.index', compact('orders'));
}
/**
* @throws \Exception
*/
public function show(MidtransService $midtransService, Order $order)
{
// get last payment
$payment = $order->payments->last();
if ($payment == null || $payment->status == 'EXPIRED') {
$snapToken = $midtransService->createSnapToken($order);
$order->payments()->create([
'snap_token' => $snapToken,
'status' => 'PENDING',
]);
} else {
$snapToken = $payment->snap_token;
}
return view('customer.orders.show', compact('order', 'snapToken'));
}
}
Membuat View
Selanjutnya, buatkan view index.blade.php dan show.blade.php. Untuk isinya, silahkan lihat di: https://github.com/mulyosyahidin/laravel-midtrans/tree/main/resources/views/customer/orders
Saya akan menjelaskan sedikit mengenai show.blade.php
Perhatikan bagian berikut:
Pada baris ke 62, kita perlu memberikan client key yang tadi sudah kita dapatkan dari dashboard Midtrans. Kemudian, pada baris ke-69 kita menggunakan snap token yang dihasilkan oleh controller.
Membuat Callback
Untuk membuat callback, buatkan controller baru.
php artisan make:controller PaymentController
class PaymentController extends Controller
{
public function midtransCallback(Request $request, MidtransService $midtransService)
{
if ($midtransService->isSignatureKeyVerified()) {
$order = $midtransService->getOrder();
if ($midtransService->getStatus() == 'success') {
$order->update([
'status' => 'processing',
'payment_status' => 'paid',
]);
$lastPayment = $order->payments()->latest()->first();
$lastPayment->update([
'status' => 'PAID',
'paid_at' => now(),
]);
}
if ($midtransService->getStatus() == 'pending') {
// lakukan sesuatu jika pembayaran masih pending, seperti mengirim notifikasi ke customer
// bahwa pembayaran masih pending dan harap selesai pembayarannya
}
if ($midtransService->getStatus() == 'expire') {
// lakukan sesuatu jika pembayaran expired, seperti mengirim notifikasi ke customer
// bahwa pembayaran expired dan harap melakukan pembayaran ulang
}
if ($midtransService->getStatus() == 'cancel') {
// lakukan sesuatu jika pembayaran dibatalkan
}
if ($midtransService->getStatus() == 'failed') {
// lakukan sesuatu jika pembayaran gagal
}
return response()
->json([
'success' => true,
'message' => 'Notifikasi berhasil diproses',
]);
} else {
return response()->json([
'message' => 'Unauthorized',
], 401);
}
}
}
Pada callback ini, kita bisa menangani status seperti berhasil, pending, expire, batal dan gagal. Method ini seharusnya hanya bisa diakses oleh Midtrans, karena kita memberikan validasi berupa pemeriksaan isSignatureKeyVerified()
sehingga jika seseorang tidak tahu signature key nya, maka tidak akan bisa mengakses callback ini.
Routes
<?php
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::get('/customer/orders', [App\Http\Controllers\Customer\OrderController::class, 'index'])->name('customer.orders.index');
Route::get('/customer/orders/{order}', [App\Http\Controllers\Customer\OrderController::class, 'show'])->name('customer.orders.show');
Route::post('/payment/midtrans-callback', [App\Http\Controllers\PaymentController::class, 'midtransCallback']);
Silahkan sesuaikan route nya dengan kebutuhan kamu, misalnya memberikan middleware pada route order. Harap diingat, route untuk callback tidak boleh memiliki authentikasi apapun.
Uji Coba
Jika kamu perhatikan, saat ini kita masih melakukan development di localhost. Oleh karena itu, Laravel kita juga masih berada di komputer lokal kita dan belum bisa diakses dari internet.
Masalahnya dimana? Perhatikan pada route untuk callback Midtrans, yang akan menghasilkan: http://localhost:8000/payment/midtrans-callback
. Route ini, tentu tidak bisa diakses oleh Midtrans karena masih berada di localhost. Kita perlu mengupload kode kita ke server supaya bisa diakses dari internet dan Midtrans bisa mengirimkan callback.
Namun, selama tahap development, kita bisa menggunakan aplikasi tunnel seperti localhost.run, atau Ngrok.
Ngrok
Ngrok adalah aplikasi dimana kita bisa membuat localhost kita menjadi online dapat diakses siapapun dari internet. Silahkan install Ngrok di komputer kamu. Ngrok: https://ngrok.com/
Membuat local server
Pertama, jalankan php artisan serve
untuk membuat Laravel kita berjalan pada port 8000. Jika kamu menggunakan Laragon di Windows, kamu tetap perlu menjalankan perintah ini.
Menjalankan Ngrok
Jika Ngrok sudah terinstal, jalankan perintah: ngrok http 8000
, maka Ngrok akan memberikan URL yang bisa diakses dari internet. Untuk membuka dasbor Ngrok, buka localhost:4040
di browser kamu. Berikut tampilan dasbor Ngrok jika belum ada request yang masuk.
Setting URL Notifikasi di Midtrans
Selanjutnya, kita perlu mengatur URL Notifikasi di dasbor Midtrans.
Jika web kamu online, jangan lupa domain yang diberikan Ngrok menjadi domain kamu.
Uji coba
Pertama, akses localhost:8000/customer/orders
untuk melihat semua data order. Kemudian pilih salah satu order.
Disini, terdapat order dengan ID ORD11641731018669 yang belum dibayar. Klik tombol Bayar Sekarang untuk menampilkan interface SNAP API.
Pada gambar 8, saya memilih pembayaran melalui BCA VA.
Melakukan simulasi pembayaran
Karena masih dalam tahap development, tentu kita tidak perlu benar-benar melakukan pembayaran. Untuk melakukan pembayaran, kita bisa menggunakan simulator pembayaran yang disediakan Midtrans. Silahkan buka di: https://simulator.sandbox.midtrans.com/bca/va/payment
Pada kolom nomor VA, silahkan paste nomor VA yang didapatkan dari SNAP API. Kamu juga bebas memilih channel pembayaran manapun. Kemudian klik Inquire. Pada halaman konfirmasi selanjutnya, klik saya Pay untuk melakukan pembayaran.
Silahkan buka dasbor Ngrok kamu. Perhatikan bahwa Midtrans mengirimkan callback dengan data-data seperti channel pembayaran, status, dan lain sebagainya. Data-data itulah yang diproses di PaymentController tadi.
Selanjutnya, refresh halaman order tadi, dan lihatlah bahwa status pembayaran sudah berubah menjadi paid secara otomatis.
Untuk diperhatikan
Ada beberapa hal yang perlu kamu ingat. Pertama, jika kamu mematikan tunnel Ngrok atau mematikan komputer, dan kamu perlu melakukan pembayaran lagi, kamu perlu membuat tunnel baru dengan perintah yang sama, yaitu: ngrok http 8000
. 8000
ini dapat diganti dengan port berapapun Laravel kamu berjalan. Jika sudah mendapatkan URL tunnel baru, jangan lupa update URL Notifikasinya di dasbor Midtrans.
Kedua, jika sudah dalam tahap produksi, jangan lupa untuk mengubah access key di file environment.
Selesai…
Sampai disini, Midtrans seharusnya sudah bisa diintegrasikan ke Laravel. Implementasi tersebut cukup bahkan sampai ke tahap produksi sekalipun untuk menerima pembayaran nyata.
Jika kamu paham konsepnya, Midtrans bisa diimplementasikan untuk kasus lain misal website donasi, spp sekolah, hingga iuran warga.
Jika terdapat error atau ada hal yang belum dipahami, silahkan tanyakan di kolom komentar.