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

Pakasir Sdk Laravel Package

fadhila36/pakasir-sdk

A modern, type-safe Laravel SDK for the Pakasir payment gateway with QRIS, multi-bank Virtual Account, and PayPal support, plus automatic fees, retries, DTOs, events, and secure webhook verification.

View on GitHub
Deep Wiki
Context7

Getting Started

Prerequisites check first. The package is PHP ^8.3 and Laravel ^11.0 only (v2.0.0 dropped 8.1/8.2 and Laravel 10). If your app is still on Laravel 10 or PHP 8.2, you cannot use v2 — you'd be pinned to the older v1 line with an entirely different (array-based) API. Confirm this before anything else.

composer require fadhila36/pakasir-sdk
php artisan vendor:publish --provider="Fadhila36\Pakasir\PakasirServiceProvider" --tag="config"
PAKASIR_PROJECT=slug-proyek-anda
PAKASIR_API_KEY=api-key-rahasia-anda
PAKASIR_TIMEOUT=30
PAKASIR_RETRY_ATTEMPTS=3
PAKASIR_LOGGING_ENABLED=true

Then php artisan config:clear — these are read through config/pakasir.php, so a stale config cache is the #1 source of "my credentials are ignored".

Where to look first, in order:

  1. config/pakasir.php after publishing — this is the real contract, and the README only documents five of its keys. Read the whole file; there may be base-URL/sandbox toggles not mentioned in the docs.
  2. docs/INTEGRATION.md in the repo — the README is a feature brochure; the integration guide is where the actual flow lives.
  3. The DTO classes in vendor/fadhila36/pakasir-sdk/src/ (TransactionCreateResponse, TransactionDetailResponse, WebhookPayload). They're readonly and typed, so the source is the API reference — every property you can read is visible there.

First use case to wire up: a single QRIS checkout, end to end.

use Fadhila36\Pakasir\Enums\PaymentMethod;
use Fadhila36\Pakasir\Facades\Pakasir;

$transaction = Pakasir::createPayment(
    paymentMethod: PaymentMethod::QRIS,
    orderId: $order->code,
    amount: (int) $order->amount,
    redirectUrl: route('checkout.finish', $order),
);

$order->payment()->create([
    'payment_number' => $transaction->paymentNumber, // QRIS string / VA number
    'payment_url'    => $transaction->paymentUrl,
    'total_payment'  => $transaction->totalPayment,  // amount + fee
    'method'         => PaymentMethod::QRIS->value,
]);

return redirect($transaction->paymentUrl);

Do this in a sandbox project before touching production. Verify the fee actually lands in totalPayment for the method you picked, and confirm the QR renders from paymentNumber (see QRISHelper below).

Implementation Patterns

Wrap the facade in your own service. Calling Pakasir:: directly from controllers is fine for a spike, but the moment you add a second payment method or a retry policy you'll want a seam:

final class CheckoutService
{
    public function __construct(private readonly PakasirInterface $pakasir) {}

    public function start(Order $order, PaymentMethod $method): Payment
    {
        $tx = $this->pakasir->createPayment($method, $order->code, (int) $order->amount);
        // persist + return
    }
}

The package registers Fadhila36\Pakasir\Contracts\PakasirInterface in the container (per the v2 release notes), so constructor injection and test fakes work without touching the facade.

The webhook handler is the pattern that matters. Copy this shape, don't improvise:

public function __invoke(Request $request)
{
    $payload = $request->all();

    $order = Order::where('code', $payload['order_id'] ?? null)->firstOrFail();

    try {
        $webhook = Pakasir::verifyWebhook($payload, (int) $order->amount);
    } catch (WebhookValidationException $e) {
        Log::warning('pakasir.webhook.rejected', ['payload' => $payload, 'reason' => $e->getMessage()]);
        return response()->json(['message' => 'invalid'], 400);
    }

    DB::transaction(function () use ($order, $webhook) {
        $order = Order::lockForUpdate()->find($order->id);

        if ($order->paid_at) {
            return; // idempotent — gateways retry aggressively
        }

        $order->update([
            'status'       => $webhook->status->value,
            'paid_at'      => $webhook->completedAt,
        ]);
    });

    return response()->json(['status' => 'ok']);
}

Two things make this work: the expectedAmount comes from your database, never from the payload; and the lockForUpdate + paid_at guard handles duplicate deliveries.

Keep a polling fallback. Webhooks get lost — local dev (no public URL), misconfigured firewalls, gateway outages. A scheduled command that re-checks pending orders older than N minutes uses detailPayment($order->code, $order->amount) and reconciles. This is also your recovery path when a webhook is rejected mid-deploy.

Use events for fulfilment, not for payment state. TransactionCreated, PaymentCompleted, TransactionCanceled, WebhookReceived are dispatched by the SDK. Let listeners handle side effects (send receipt, decrement stock, notify ops) — but treat your own DB write as the source of truth, and implement ShouldQueue on every listener. A throwing listener inside the SDK's dispatch path can turn a successful payment creation into a 500 for the customer.

Payment links via notification. PaymentLinkNotification exists for emailing/database-notifying the paymentUrl. Useful for the "resend invoice" screen, which every Indonesian payment integration ends up needing.

Enum-driven UI. Build your method selector from PaymentMethod::cases() and use the built-in fee/minimum logic to render "Rp50.000 + Rp3.500 = Rp53.500" before the customer commits. This is the single biggest UX win the package offers — surfacing fees upfront instead of surprising people on paymentUrl.

Testing. Http::fake() works here because the SDK goes through Laravel's HTTP client; it's essential for verifyWebhook(), which makes a real back-channel call. For unit tests, bind a fake PakasirInterface. Use simulationPayment() to drive a full happy path in sandbox without a human tapping a QR.

Gotchas and Tips

amount is part of the lookup key, not just the creation payload. detailPayment(), cancelPayment(), and simulationPayment() all take (string $orderId, int|float $amount). If the amount you pass doesn't match what the transaction was created with, these calls fail. Persist the exact integer you sent — never recompute it from $order->items later, because a line item edit silently breaks refunds and reconciliations. Store amount, fee, and total_payment as separate columns.

Retries are dangerous on createPayment(). PAKASIR_RETRY_ATTEMPTS=3 retries failed requests automatically. For detailPayment that's exactly right. For createPayment, a timeout-then-retry can leave you with two open transactions for one order, only one of which the customer pays. Set retries low (1–2) and rely on orderId uniqueness/your own guard, or disable retries for creation specifically.

verifyWebhook() costs a network round trip. It validates the payload, then calls Pakasir to confirm the transaction is genuine — because Pakasir webhooks carry no cryptographic signature. That's the right security decision, but it means your webhook endpoint has real latency and can fail if Pakasir is slow. Keep PAKASIR_TIMEOUT well under your webhook gateway's timeout, and consider acking fast and doing the verification in a queued job if you're on a strict ingress timeout.

Configure the webhook route correctly. POST only, excluded from CSRF (bootstrap/app.php → validateCsrfTokens(except: ['webhooks/pakasir'])), no auth middleware, and throttling that won't lock out legitimate retry bursts. A 419 or 401 here reads as "webhook broken" and, without the polling fallback, means silent unpaid orders.

Logging is on by default in the README example. PAKASIR_LOGGING_ENABLED=true writes outbound and inbound payment traffic to laravel.log. Turn it off in production or route the channel elsewhere — you're logging payment payloads and possibly API-adjacent data into a file that often ends up in shared log aggregation.

Money types. The signature accepts int|float. Never pass a float for IDR. Cast at the boundary with (int), and validate against the method's minimum before calling — PayPal's fee is 1% (min 3.000) and the VA methods have their own thresholds. A too-small amount throws from Pakasir; catch it and show a friendly validation message rather than a 500.

PaymentMethod::ALL is almost certainly not valid for createPayment(). It's a code (all) for filtering/listing. Only pass concrete methods when creating; if you need a "let the customer choose" page, iterate cases() and skip ALL.

Don't hand-render the QRIS string. paymentNumber for QRIS is an EMVCo payload; use the bundled QRISHelper for sanitation/formatting and feed it to a JS QR renderer client-side rather than embedding raw text into an <img>. Also honour expiry — QRIS transactions don't live forever, so show a countdown and a "buat ulang" path calling createPayment again.

Version pinning. The release history is a yellow flag: v2.0.0 is the modern line, but a v1.0.1 release was published afterwards with a changelog comparing v2.0.0...v1.0.1. With 4 stars and 0 dependents, treat every version bump as unvetted — pin exactly in composer.json, read the diff, and keep a sandbox smoke test (create → simulate → webhook verify) in your CI so an upgrade that changes DTO property names fails loudly instead of in production.

Own your schema. There are no migrations in this package. You are responsible for orders_payments: order_id, method, amount, fee, total_payment, payment_number, status, paid_at, and ideally a raw_response JSON column — you'll want the original payload the first time a customer disputes a charge.

Extension point worth knowing: since the facade resolves PakasirInterface, you can decorate it — a LoggingPakasir implements PakasirInterface that wraps the real client and records every call per order. That's usually a better audit trail than the built-in logger and doesn't require touching vendor code.

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.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel