Midtrans is Indonesia's leading payment gateway solution that provides comprehensive payment processing services across Southeast Asia. This integration supports multiple payment methods including credit/debit cards, e-wallets (GoPay, ShopeePay), bank transfers, QRIS, and convenience store payments, making it ideal for businesses operating in Indonesia and the broader Southeast Asian market.
| Country | Currency | Code | Primary Payment Methods |
|---|---|---|---|
| Indonesia | Indonesian Rupiah | IDR | GoPay, ShopeePay, QRIS, Bank Transfer, Credit Cards |
| Singapore | Singapore Dollar | SGD | Credit/Debit Cards, PayNow |
| Malaysia | Malaysian Ringgit | MYR | Credit/Debit Cards, FPX |
| Vietnam | Vietnamese Dong | VND | Credit/Debit Cards, MoMo |
| Philippines | Philippine Peso | PHP | Credit/Debit Cards, GCash |
| Thailand | Thai Baht | THB | Credit/Debit Cards, PromptPay |
| Method | Code | Description |
|---|---|---|
| Credit/Debit Card | credit_card | Visa, Mastercard, JCB, Amex |
| GoPay | gopay | GoJek's digital wallet |
| ShopeePay | shopeepay | Shopee's digital wallet |
| QRIS | qris | Indonesia's QR code payment standard |
| Bank Transfer | bank_transfer | Virtual account transfers |
| BCA KlikPay | bca_klikpay | BCA's online payment platform |
| BCA KlikBCA | bca_klikbca | BCA's internet banking |
| BRI e-Pay | bri_epay | BRI's online payment |
| Indomaret | indomaret | Convenience store payment |
| Alfamart | alfamart | Convenience store payment |
composer require midtrans/midtrans-php
Add these variables to your .env file:
# Midtrans Configuration
MIDTRANS_SERVER_KEY=YOUR_SERVER_KEY
MIDTRANS_CLIENT_KEY=YOUR_CLIENT_KEY
MIDTRANS_TEST_MODE=true
MIDTRANS_WEBHOOK_URL=https://yourapp.com/payment/midtrans/webhook
MIDTRANS_RETURN_URL=https://yourapp.com/payment/midtrans/success
php artisan vendor:publish --provider="Mdiqbal\LaravelPayments\PaymentsServiceProvider"
// config/payments.php
'gateways' => [
'midtrans' => [
'driver' => 'midtrans',
'server_key' => env('MIDTRANS_SERVER_KEY'),
'client_key' => env('MIDTRANS_CLIENT_KEY'),
'test_mode' => env('MIDTRANS_TEST_MODE', true),
'webhook_url' => env('MIDTRANS_WEBHOOK_URL'),
'return_url' => env('MIDTRANS_RETURN_URL'),
'enabled_payments' => [
'credit_card',
'gopay',
'shopeepay',
'bank_transfer',
'qris',
],
],
],
Once approved:
In your Midtrans dashboard:
https://yourapp.com/payment/midtrans/webhookuse Mdiqbal\LaravelPayments\Facades\Payment;
use Mdiqbal\LaravelPayments\DTOs\PaymentRequest;
// Initialize Midtrans gateway
$payment = Payment::gateway('midtrans');
// Create a payment request
$response = $payment->pay(new PaymentRequest(
amount: 50000, // IDR 50,000
currency: 'IDR',
orderId: 'ORDER-' . uniqid(),
description: 'Product purchase',
customer: [
'name' => 'Budi Santoso',
'email' => 'budi@example.com',
'phone' => '+628123456789',
'address' => 'Jl. Sudirman No. 123',
'city' => 'Jakarta',
'country' => 'IDN',
'postal_code' => '12345',
],
returnUrl: route('payment.success'),
notifyUrl: route('payment.webhook'),
metadata: [
'credit_card' => [
'secure' => true,
'bank' => 'bca',
'installment' => [
'required' => false,
],
],
'items' => [
[
'id' => 'PROD1',
'price' => 30000,
'quantity' => 1,
'name' => 'Product A',
'brand' => 'Brand A',
'category' => 'Electronics',
'merchant_name' => 'Your Store',
'url' => 'https://yourstore.com/product1',
'image_url' => 'https://yourstore.com/images/product1.jpg',
],
[
'id' => 'PROD2',
'price' => 20000,
'quantity' => 1,
'name' => 'Product B',
'brand' => 'Brand B',
'category' => 'Accessories',
'merchant_name' => 'Your Store',
'url' => 'https://yourstore.com/product2',
'image_url' => 'https://yourstore.com/images/product2.jpg',
]
],
]
));
if ($response->success) {
// Store the snap token for later use
session(['snap_token' => $response->data['snap_token']]);
// Redirect to Midtrans payment page
return redirect($response->redirectUrl);
} else {
// Handle error
return back()->with('error', $response->message);
}
// Limit payment options to GoPay and ShopeePay only
$response = $payment->pay(new PaymentRequest(
amount: 25000,
currency: 'IDR',
orderId: 'EWALLET-' . uniqid(),
description: 'E-wallet payment',
customer: [
'name' => 'Siti Nurhaliza',
'email' => 'siti@example.com',
'phone' => '+628987654321',
],
metadata: [
'credit_card' => [
'secure' => true,
],
'expiry' => [
'unit' => 'minutes',
'duration' => 30,
],
]
));
// In your Midtrans configuration, set:
// 'enabled_payments' => ['gopay', 'shopeepay']
$linkResponse = $payment->createPaymentLink([
'amount' => 100000,
'order_id' => 'INVOICE-' . time(),
'currency' => 'IDR',
'description' => 'Monthly subscription fee',
'customer_name' => 'Ahmad Fadli',
'customer_email' => 'ahmad@example.com',
'customer_phone' => '+6281122334455',
'expiry_duration' => 72, // 72 hours
'items' => [
[
'id' => 'SUBSCRIPTION',
'price' => 100000,
'quantity' => 1,
'name' => 'Monthly Subscription',
'category' => 'Service',
]
],
'customer_address' => [
'first_name' => 'Ahmad',
'last_name' => 'Fadli',
'address' => 'Jl. Gatot Subroto No. 456',
'city' => 'Bandung',
'postal_code' => '40123',
'country_code' => 'IDN',
],
'redirect_url' => route('payment.success'),
'callback_url' => route('payment.webhook'),
]);
if ($linkResponse->success) {
$paymentUrl = $linkResponse->redirectUrl;
// Send payment link via WhatsApp, email, or SMS
// Example: WhatsApp share link
$whatsappUrl = 'https://wa.me/?text=' . urlencode(
"Halo! Silakan lakukan pembayaran Anda melalui link berikut: " . $paymentUrl
);
return redirect($whatsappUrl);
}
// Check payment status using order ID
$orderId = 'ORDER-123456789';
$response = $payment->verify(['order_id' => $orderId]);
if ($response->success) {
echo "Payment Status: " . $response->status;
echo "Transaction ID: " . $response->transactionId;
echo "Payment Type: " . $response->data['payment_type'];
echo "Gross Amount: " . $response->data['gross_amount'];
echo "Currency: " . $response->data['currency'];
echo "Fraud Status: " . $response->data['fraud_status'];
if ($response->status === 'completed') {
// Update order status
// Send confirmation email
// Process order fulfillment
}
}
// Process a refund
$refundResponse = $payment->refund([
'transaction_id' => 'MIDTRANS-123456789',
'amount' => 25000, // Partial refund or null for full refund
'reason' => 'Customer requested refund'
]);
if ($refundResponse->success) {
echo "Refund processed successfully";
echo "Refund Key: " . $refundResponse->data['refund_key'];
echo "Refund Amount: " . $refundResponse->data['refund_amount'];
}
// GoPay specific configuration
$payment->pay(new PaymentRequest(
amount: 50000,
currency: 'IDR',
orderId: 'GOPAY-' . uniqid(),
customer: [
'name' => 'Customer Name',
'phone' => '+628123456789', // Required for GoPay
],
metadata: [
'items' => [
[
'id' => 'ITEM1',
'price' => 50000,
'quantity' => 1,
'name' => 'Product',
'category' => 'Digital Goods',
'url' => 'https://yourstore.com/product',
'image_url' => 'https://yourstore.com/images/product.jpg',
]
],
'credit_card' => [
'save_card' => false,
'secure' => true,
],
'enabled_payments' => ['gopay'],
]
));
// routes/web.php
Route::post('/payment/midtrans/webhook', [MidtransController::class, 'handleWebhook'])
->name('payment.midtrans.webhook')
->middleware('midtrans.webhook');
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Mdiqbal\LaravelPayments\Facades\Payment;
class MidtransController extends Controller
{
/**
* Handle Midtrans webhook
*/
public function handleWebhook(Request $request)
{
$gateway = Payment::gateway('midtrans');
// Process webhook
$response = $gateway->processWebhook($request->all());
if ($response->success) {
// Extract webhook data
$webhookData = $response->data;
$status = $response->status;
// Process based on status
switch ($status) {
case 'completed':
$this->handleSuccessfulPayment($webhookData);
break;
case 'pending':
$this->handlePendingPayment($webhookData);
break;
case 'failed':
case 'cancelled':
$this->handleFailedPayment($webhookData);
break;
case 'refunded':
$this->handleRefund($webhookData);
break;
}
return response('Webhook processed successfully');
}
return response('Webhook processing failed', 400);
}
/**
* Handle successful payment
*/
private function handleSuccessfulPayment(array $data)
{
// Update your database
DB::table('payments')
->where('order_id', $data['order_id'])
->update([
'status' => 'completed',
'midtrans_transaction_id' => $data['transaction_id'],
'payment_type' => $data['payment_type'] ?? null,
'gross_amount' => $data['gross_amount'] ?? 0,
'currency' => $data['currency'] ?? 'IDR',
'fraud_status' => $data['fraud_status'] ?? null,
'approval_code' => $data['approval_code'] ?? null,
'bank' => $data['bank'] ?? null,
'masked_card' => $data['masked_card'] ?? null,
'card_type' => $data['card_type'] ?? null,
'settlement_time' => $data['settlement_time'] ?? null,
'paid_at' => now()
]);
// Update order status
DB::table('orders')
->where('id', $data['order_id'])
->update(['status' => 'paid']);
// Send confirmation email
// Generate receipt
// Trigger fulfillment process
}
/**
* Handle pending payment
*/
private function handlePendingPayment(array $data)
{
DB::table('payments')
->where('order_id', $data['order_id'])
->update([
'status' => 'pending',
'pending_at' => now()
]);
// Log pending payment
// Send follow-up email
}
/**
* Handle failed payment
*/
private function handleFailedPayment(array $data)
{
DB::table('payments')
->where('order_id', $data['order_id'])
->update([
'status' => 'failed',
'failure_reason' => $data['status_message'] ?? 'Payment failed',
'failed_at' => now()
]);
// Notify customer
// Log the failure for review
}
/**
* Handle refund
*/
private function handleRefund(array $data)
{
DB::table('refunds')
->where('order_id', $data['order_id'])
->update([
'status' => 'completed',
'processed_at' => now()
]);
// Notify customer
// Update inventory if needed
}
}
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class MidtransWebhook
{
public function handle(Request $request, Closure $next)
{
// Log webhook for debugging
Log::info('Midtrans webhook received', [
'ip' => $request->ip(),
'payload' => $request->all()
]);
// You can add IP filtering here if needed
$allowedIps = [
'103.20.50.155', // Midtrans IP
'103.20.50.157', // Midtrans IP
// Add more IPs as provided by Midtrans
];
if (!in_array($request->ip(), $allowedIps)) {
Log::warning('Unauthorized webhook attempt', [
'ip' => $request->ip()
]);
abort(403, 'Unauthorized');
}
return $next($request);
}
}
<!DOCTYPE html>
<html>
<head>
<title>Payment with Midtrans</title>
<script src="https://app.sandbox.midtrans.com/snap/snap.js"
data-client-key="{{ $clientKey }}"></script>
<style>
.payment-button {
background-color: #4CAF50;
color: white;
padding: 14px 20px;
border: none;
cursor: pointer;
width: 100%;
font-size: 16px;
}
.payment-button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<h2>Complete Your Payment</h2>
<p>Amount: Rp {{ number_format($amount, 0, ',', '.') }}</p>
<button id="pay-button" class="payment-button">Pay Now</button>
<script>
document.getElementById('pay-button').onclick = function() {
snap.pay('{{ $snapToken }}', {
onSuccess: function(result) {
console.log('Payment success:', result);
window.location.href = '{{ route("payment.success") }}';
},
onPending: function(result) {
console.log('Payment pending:', result);
window.location.href = '{{ route("payment.pending") }}';
},
onError: function(result) {
console.log('Payment error:', result);
window.location.href = '{{ route("payment.error") }}';
},
onClose: function() {
console.log('Customer closed the popup without finishing the payment');
window.location.href = '{{ route("payment.cancel") }}';
}
});
};
</script>
</body>
</html>
// Example in Vue.js
export default {
methods: {
payWithMidtrans() {
const snapScript = document.createElement('script');
snapScript.src = 'https://app.sandbox.midtrans.com/snap/snap.js';
snapScript.setAttribute('data-client-key', this.clientKey);
snapScript.onload = () => {
window.snap.pay(this.snapToken, {
onSuccess: (result) => {
this.$emit('payment-success', result);
},
onPending: (result) => {
this.$emit('payment-pending', result);
},
onError: (result) => {
this.$emit('payment-error', result);
}
});
};
document.head.appendChild(snapScript);
}
}
}
Midtrans provides a sandbox environment:
MIDTRANS_TEST_MODE=true
Get test credentials from Midtrans:
Use these test cards for testing:
| Card Type | Number | Expiry | CVV | Result |
|---|---|---|---|---|
| Visa Success | 4811111111111114 | Any future | Any | Success |
| Visa 3DS | 4811111111111116 | Any future | Any | Challenge |
| Visa Failure | 4811111111111115 | Any future | Any | Denied |
| Mastercard | 5111111111111116 | Any future | Any | Success |
| JCB | 3511111111111116 | Any future | Any | Success |
// Test GoPay payment
$testGoPay = $payment->pay(new PaymentRequest(
amount: 10000,
currency: 'IDR',
orderId: 'TEST-GOPAY-' . time(),
description: 'Test GoPay payment',
customer: [
'name' => 'Test User',
'phone' => '+628123456789',
],
metadata: [
'enabled_payments' => ['gopay'],
]
));
// Never expose server-side keys to frontend
// Use client_key in JavaScript, server_key in backend
// Good: In Blade template
<script src="https://app.sandbox.midtrans.com/snap/snap.js"
data-client-key="{{ config('services.midtrans.client_key') }}"></script>
// Bad: Exposing server key
<script>
const serverKey = '{{ config('services.midtrans.server_key') }}'; // NEVER DO THIS
</script>
// Always validate order amounts before payment
if ($order->total != $request->amount) {
abort(403, 'Invalid amount');
}
// Verify order ownership
if ($order->user_id != auth()->id()) {
abort(403, 'Unauthorized');
}
| Code | Description | Solution |
|---|---|---|
| 400 | Bad Request | Check request parameters |
| 401 | Unauthorized | Verify Server Key |
| 402 | Duplicate Order ID | Use unique order IDs |
| 404 | Not Found | Check transaction status |
| 410 | Expired | Create new payment |
| 411 | Invalid Currency | Use supported currency |
| 412 | Invalid Amount | Check amount format |
try {
$response = $payment->pay($paymentRequest);
if (!$response->success) {
// Log error details
Log::error('Midtrans payment failed', [
'error_code' => $response->errorCode,
'message' => $response->message,
'order_id' => $paymentRequest->orderId
]);
// Show user-friendly message
$userMessage = $this->getUserFriendlyErrorMessage($response->errorCode);
return back()->with('error', $userMessage);
}
} catch (\Exception $e) {
Log::error('Midtrans gateway error', [
'error' => $e->getMessage()
]);
return back()->with('error', 'Payment service temporarily unavailable.');
}
private function getUserFriendlyErrorMessage(string $errorCode): string
{
$errorMessages = [
'410' => 'Payment link has expired. Please try again.',
'411' => 'Currency not supported. Please use IDR.',
'412' => 'Invalid amount format. Please check the amount.',
'413' => 'Invalid payment method. Please try another option.',
'414' => 'Card not authorized. Please use another card.',
];
return $errorMessages[$errorCode] ?? 'Payment failed. Please try again.';
}
How can I help you explore Laravel packages today?