laravel-midtrans maintained by khumam
Laravel Midtrans
Laravel package for Midtrans payment gateway integration. Supports Laravel 11, 12, and 13.
Inspired by Laravel Cashier (Paddle). Simplified flow: checkout and webhook handling.
Installation
composer require khumam/laravel-midtrans
Publish config and migrations:
php artisan vendor:publish --tag=midtrans-config
php artisan vendor:publish --tag=midtrans-migrations
php artisan migrate
Add to .env:
MIDTRANS_SERVER_KEY=SB-Mid-server-xxxx
MIDTRANS_IS_SANDBOX=true
Setup
Add the Billable trait to your User model (or any billable model):
use Khumam\Midtrans\Billable;
class User extends Model
{
use Billable;
}
Usage
One-Time Payment
use Khumam\Midtrans\Billable;
// In your controller
public function pay(Request $request)
{
return $request->user()
->checkout(50000)
->withItemDetail([
['id' => 'item1', 'price' => 50000, 'quantity' => 1, 'name' => 'Product A'],
])
->secureCreditCard()
->redirectTo('payment.success');
}
The checkout() method:
- Cancels any existing pending transactions for the user
- Creates a new transaction record
- Calls the Midtrans Snap API
- Redirects user to the Midtrans payment page
Subscription
use Khumam\Midtrans\Enums\MidtransPeriod;
// Monthly subscription
return $user->subscribe(100000, MidtransPeriod::Monthly)
->withSubscriptionSchedule([
'interval' => 1,
'interval_unit' => 'month',
])
->redirectTo('subscription.success');
// Annual subscription
return $user->subscribe(1000000, MidtransPeriod::Annually)
->redirectTo('subscription.success');
Available periods:
| Period | Enum Value |
|---|---|
| Monthly | MidtransPeriod::Monthly |
| Quarterly | MidtransPeriod::Quarterly |
| Semi-Annually | MidtransPeriod::SemiAnnually |
| Annually | MidtransPeriod::Annually |
Check Subscription Status
// Check if user has active subscription
if ($user->subscribed()) {
// User is subscribed
}
// Get the active subscription transaction
$subscription = $user->subscription();
// Returns Transaction model or null
Checkout Builder Methods
All methods below are chainable on the Checkout object returned by checkout() or subscribe().
Payment Options
->secureCreditCard() // Enable 3DS/secure credit card
->withCreditCard([ // Custom credit card config
'token_id' => 'xxx',
'bank' => 'bni',
'installment_term' => 3,
])
->withBankTransfer([ // Bank transfer (VA)
'bank' => 'bca',
'va_number' => '1234567890',
])
->withGopay([ // GoPay config
'enable_callback' => true,
'callback_url' => 'https://example.com/callback',
])
->withQris([ // QRIS config
'acquirer' => 'gopay',
])
Transaction Details
->withItemDetail([
['id' => 'item1', 'price' => 50000, 'quantity' => 1, 'name' => 'Product A'],
['id' => 'item2', 'price' => 25000, 'quantity' => 2, 'name' => 'Product B'],
])
->withCustomerDetail([ // Override customer details
'first_name' => 'John',
'last_name' => 'Doe',
'email' => 'john@example.com',
'phone' => '08123456789',
'billing_address' => [
'first_name' => 'John',
'address' => 'Jl. Sudirman No. 1',
'city' => 'Jakarta',
'postal_code' => '12190',
'country_code' => 'IDN',
],
])
->withCustomExpiry([ // Custom expiration
'expiry_duration' => '60',
'unit' => 'minute',
])
Default customer details are auto-populated from the billable model's name and email fields. Override with custom field names:
$user->checkout(50000, [
'name_field' => 'full_name',
'email_field' => 'email_address',
]);
Subscription Schedule
->withSubscriptionSchedule([
'interval' => 1,
'interval_unit' => 'month',
'max_interval' => 12,
])
Tax
->withTax(5000) // Fixed tax: adds 5000 to gross_amount
->withTaxPercentage(11) // Percentage tax: 11% of gross_amount
withTax(float $taxPrice)— adds fixed tax amount togross_amount, storestax_amount, adds a "Tax" item detailwithTaxPercentage(float $percent)— calculatesgross_amount * percent / 100, adds togross_amount, storestax_amountandtax_percentage, adds a "Tax" item detail
Finalize
->redirectTo('route.name') // Redirects user to Midtrans Snap payment page
->getRedirectUrl() // Returns the Snap redirect URL as string (no redirect)
Use getRedirectUrl() when you need the URL without auto-redirecting (SPA, API responses, etc.):
// Return URL as JSON for frontend
$url = $user->checkout(50000)->getRedirectUrl();
return response()->json(['payment_url' => $url]);
// Or redirect manually
return redirect($user->checkout(50000)->getRedirectUrl());
Webhook
The package automatically registers a webhook endpoint at POST /midtrans/webhook.
Configure this URL in your Midtrans Dashboard under Settings > Payment Notification URL:
https://yourdomain.com/midtrans/webhook
Webhook Flow
- Receives POST from Midtrans
- Validates signature:
SHA512(order_id + status_code + gross_amount + server_key) - Updates transaction status
- Stores full response in
midtrans_transaction_responsestable
Transaction Statuses
| Status | Meaning |
|---|---|
capture |
Card payment captured successfully. Funds received. |
settlement |
Transaction settled. Funds credited to account. |
pending |
Awaiting customer payment. |
deny |
Payment rejected by provider or fraud system. |
cancel |
Transaction cancelled. |
expire |
Payment window expired. |
failure |
Unexpected error during processing. |
refund |
Full refund issued. |
partial_refund |
Partial refund issued. |
authorize |
Card pre-authorized (advanced feature). |
Transaction Model
use Khumam\Midtrans\Models\Transaction;
$transaction = Transaction::where('order_id', $orderId)->first();
$transaction->isPaid(); // true if capture or settlement
$transaction->isPending(); // true if pending
$transaction->isFailed(); // true if deny/cancel/expire/failure
$transaction->isRefunded(); // true if refund or partial_refund
$transaction->onGracePeriod(3); // true if ends_at passed < 3 days ago
$transaction->billable; // The User model (morph relationship)
$transaction->responses; // All webhook responses (HasMany)
$transaction->latestResponse; // Latest webhook response (HasOne)
$transaction->items; // Transaction items (HasMany TransactionItem)
$transaction->tax_amount; // Tax amount (decimal)
$transaction->tax_percentage; // Tax percentage (decimal)
Transaction Items
When you use withItemDetail(), items are persisted and linked to the transaction. Items cascade delete with the parent transaction.
use Khumam\Midtrans\Models\TransactionItem;
$transaction->items; // Collection of TransactionItem
$transaction->items->first()->name; // 'Product A'
$item = TransactionItem::find(1);
$item->transaction; // BelongsTo Transaction
Note: Must be an indexed array of items (
[[...], [...]]), not a single associative array.
Customers
Customer details are automatically upserted when withCustomerDetail() is called (happens by default on every checkout). One customer record per billable model — subsequent checkouts update the existing record.
use Khumam\Midtrans\Models\Customer;
$user->midtransCustomer; // Customer model or null
$customer = Customer::find(1);
$customer->billable; // The User model (morph relationship)
Testing
composer test
Or directly:
vendor/bin/phpunit
Writing Tests
This package uses Orchestra Testbench for Laravel package testing. Extend the base TestCase:
namespace Khumam\Midtrans\Tests\Feature;
use Khumam\Midtrans\Tests\TestCase;
class MyTest extends TestCase
{
// database migrations run automatically
// config: midtrans.server_key and midtrans.is_sandbox are pre-set
}
Test Structure
tests/
├── TestCase.php # Base test (Orchestra Testbench)
├── Unit/
│ ├── ItemDetailObjectTest # Item detail validation, types, edge cases
│ └── TaxObjectTest # Tax calculations, fixed & percentage
└── Feature/
├── TransactionItemTest # Item relations, DB, cascade delete
├── CustomerTest # Customer upsert, relations, billing/shipping
└── CheckoutRedirectTest # getRedirectUrl, redirectTo, mocked API
License
MIT