Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Midtrans Laravel Package

sawirricardo/laravel-midtrans

A Laravel package for integrating Midtrans payments. Configure credentials, create Snap transactions, handle callbacks and webhooks, and support common payment flows from your Laravel app with a simple, framework-friendly API.

View on GitHub
Deep Wiki
Context7

Getting Started

What this package is: a thin Laravel wrapper around Midtrans, the Indonesian payment gateway (Snap, Core API, notifications). With 12 stars, a single 2023 release, and a 0.48 score, treat it as a convenience binding, not a battle-tested abstraction. Budget time to read its source before you build on it.

Minimal steps

  1. Install:

    composer require sawirricardo/laravel-midtrans
    

    It pulls in midtrans/midtrans-php as the underlying SDK — that SDK is where most real behaviour lives.

  2. Publish config (verify the tag in the service provider first):

    php artisan vendor:publish --provider="Sawirricardo\LaravelMidtrans\MidtransServiceProvider"
    
  3. Set environment variables for sandbox first:

    MIDTRANS_SERVER_KEY=SB-Mid-server-xxxxxxxx
    MIDTRANS_CLIENT_KEY=SB-Mid-client-xxxxxxxx
    MIDTRANS_IS_PRODUCTION=false
    
  4. Where to look first, in this order:

    • config/midtrans.php — the actual knobs (keys, sandbox flag, base URLs).
    • The service provider — how it binds the client into the container and whether it is a singleton.
    • The Midtrans facade and its underlying service class — this is your entire API surface. Enumerate the public methods; they are almost certainly a subset of the Midtrans SDK. Anything missing, call the SDK directly.
    • Midtrans' own docs for Snap, Core API, and HTTP notifications. They are authoritative; the package is not.
  5. First use case — Snap checkout:

    • Persist an order with a unique order_id (Midtrans requires uniqueness per merchant).
    • Request a Snap token for that order from your controller/service.
    • Render Snap (redirect URL or snap.js popup) on the frontend.
    • Expose a public HTTPS webhook route for Midtrans notifications.
    • On notification, verify the signature, re-fetch status from Midtrans, then transition the order.

    The exact facade call varies; confirm it in the facade source. The shape is:

    // Confirm method name in src/Facades/Midtrans.php — the package is a thin passthrough.
    $snapToken = Midtrans::createSnapTransaction([
        'transaction_details' => [
            'order_id'     => $order->code,
            'gross_amount' => (int) $order->total, // IDR, integer
        ],
    ]);
    

Implementation Patterns

Snap token + webhook (recommended default). Let Midtrans host the payment UI; you only create a token and react to notifications. Fewer PCI concerns, less UI work, and the notification payload contains everything you need (order_id, transaction_status, fraud_status, gross_amount, signature_key).

Order state machine, not status strings. Midtrans statuses (pending, capture, settlement, deny, cancel, expire, failure, refund, partial_refund, chargeback) should map to your domain states. Keep a single PaymentStatus enum and one applyNotification() action so webhook and manual reconciliation share logic:

// app/Actions/ApplyMidtransNotification.php
public function handle(array $payload): void
{
    DB::transaction(function () use ($payload) {
        $order = Order::where('code', $payload['order_id'])->lockForUpdate()->firstOrFail();

        if (! $this->signatureIsValid($payload)) {
            abort(403);
        }

        // Notifications can arrive out of order and more than once.
        if ($order->midtrans_status === $payload['transaction_status']) {
            return;
        }

        $order->update([
            'midtrans_status'        => $payload['transaction_status'],
            'midtrans_transaction_id'=> $payload['transaction_id'],
            'paid_at'                => in_array($payload['transaction_status'], ['capture', 'settlement'], true)
                    && ($payload['fraud_status'] ?? 'accept') === 'accept'
                    ? now() : null,
        ]);
    });
}

Verify signature yourself. Don't assume the package does it. The canonical check is:

$expected = hash('sha512',
    $payload['order_id'] . $payload['status_code'] . $payload['gross_amount'] . config('midtrans.server_key')
);
hash_equals($expected, $payload['signature_key'] ?? '');

Never trust the webhook body alone for money movement. On settlement, optionally call the status endpoint to confirm before releasing goods — cheap insurance against spoofed or replayed payloads, especially if your signature check is wrong.

Service-layer wrapper. Bind your own PaymentGateway interface so tests and future gateway swaps don't touch the vendor package:

// app/Contracts/PaymentGateway.php
interface PaymentGateway { public function createToken(Order $order): string; }

// app/Services/MidtransGateway.php
final class MidtransGateway implements PaymentGateway { /* delegates to the package */ }

Then swap implementations in AppServiceProvider and fake it in tests. This is essential with a 12-star package — you want it isolated to one class.

Core API charge is available if you want a fully custom checkout (card, VA, e-wallet via direct charge). Only reach for it when Snap's UI doesn't fit; you then own tokenisation, 3DS handling, and card-data scope.

Local development. Midtrans can't call localhost. Use ngrok/expose to get a public HTTPS URL, register it as the Payment Notification URL, and add a signature-guarded artisan command to replay captured payloads (php artisan midtrans:replay storage/app/notifications/*.json). That command pays for itself fast.

Testing. Midtrans provides a sandbox simulator; combine it with your own FakePaymentGateway binding for the 90% of tests that shouldn't hit the network.

Gotchas and Tips

  • Package maintenance risk is the headline gotcha. Last release Feb 2023, 12 stars, one author. Pin the version, vendor-lock the SDK version too, and wrap everything behind your own interface so that if it dies you swap to midtrans/midtrans-php directly in one file. Midtrans' SDK is stable; the wrapper is the fragile part.
  • gross_amount is IDR and must be an integer in practice. Passing 10000.00 floats or formatted strings ("Rp 10.000") causes cryptic validation errors. Store cents/full IDR as integers in the DB too.
  • Config cache. php artisan config:cache will freeze your keys. If production shows sandbox behaviour, php artisan config:clear and check the env names match config/midtrans.php exactly.
  • Sandbox vs production base URLs. The isProduction flag switches Snap/Core endpoints. Mixing a production key with isProduction=false gives confusing auth failures, not a clear error.
  • Notifications are the source of truth, not the browser redirect. Snap's finish redirect fires even when the payment fails or the user abandons. Never mark paid from the redirect — only from a verified notification.
  • Duplicate and out-of-order notifications are normal. Use lockForUpdate (or a unique constraint on transaction_id + a processed-events table) and make the handler idempotent. Midtrans retries until it gets a 2xx.
  • Return 200 quickly. Do the minimum synchronous work; queue anything heavy. A slow handler looks like a failure and Midtrans will retry, amplifying load.
  • order_id reuse is forbidden. Generate it per attempt or append an attempt suffix; reusing a paid order number will be rejected and confuse reconciliation.
  • Fraud status matters. For cards, transaction_status=capture with fraud_status=challenge means not yet paid. Only accept (with capture/settlement) is settled.
  • The client key is public, the server key is not. Ship the client key to the frontend for Snap; never let the server key leave the backend. Check the package doesn't log request payloads at debug level.
  • Timeouts and retries. Set sane HTTP timeouts on the underlying client; default cURL timeouts can hang a checkout. If the package exposes no timeout config, override the binding in your service provider.
  • Extension points are thin. If the package is a passthrough (likely), the cleanest extension is to resolve the underlying SDK from the container or call Midtrans:: methods directly; don't fork the wrapper for small gaps. Where you need custom behaviour — signing, retries, logging, multi-merchant — write it in your own service and call the package only for transport.
  • Multi-tenant / multiple merchants. A singleton binding plus one config only fits a single merchant. For multi-merchant apps, resolve the SDK per-tenant from an array config you maintain yourself; don't try to force it through the package's config.
  • Debug order of operations when a payment "disappears": (1) does a notification row exist in storage/logs/laravel.log? (2) is the signature check passing? (3) does the status endpoint show settlement? (4) did your signature compare use hash_equals and the unformatted gross_amount from the payload? That checklist resolves most incidents.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator