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 Bkash Laravel Package

sabitahmad/laravel-bkash

Integrate bKash payments into Laravel with full API v1.2.0 support: tokenized and regular checkout, agreements, refunds, queries, token refresh, transaction logging, events, and sandbox/production modes.

View on GitHub
Deep Wiki
Context7

Getting Started

This package wraps bKash Tokenized Checkout API v1.2.0 behind a Bkash facade. For day-to-day use, treat the README as the API reference and the published config as the source of truth.

Minimal setup

composer require sabitahmad/laravel-bkash

php artisan vendor:publish --tag="laravel-bkash-config"
php artisan vendor:publish --tag="laravel-bkash-migrations"
php artisan migrate

Add credentials to .env:

BKASH_SANDBOX=true
BKASH_APP_KEY=
BKASH_APP_SECRET=
BKASH_USERNAME=
BKASH_PASSWORD=

BKASH_CALLBACK_URL=/bkash/callback
BKASH_REDIRECT_URL=/payment/redirect
BKASH_LOG_TRANSACTIONS=true

Start by reading these published files in this order:

  1. config/bkash.php — credentials, base URLs, endpoints, retry/timeout, validation limits.
  2. The package service class behind SabitAhmad\Bkash\Facades\Bkash — how token refresh and HTTP calls are made.
  3. SabitAhmad\Bkash\Responses\* — which fields are normalized and which are raw.
  4. The migration — what transaction data is logged by default.
  5. examples/BkashController.php mentioned in the README — closest thing to a full reference flow.

First useful flow: a regular checkout

Create a payment, store the returned paymentID against your order, then redirect the customer to bKash:

use SabitAhmad\Bkash\Facades\Bkash;
use SabitAhmad\Bkash\Exceptions\BkashException;

try {
    $payment = Bkash::createPayment(
        payerReference: 'CUST-001',
        amount: 100.50,
        invoiceNumber: 'INV-001',
    );

    if (! $payment->isSuccess()) {
        throw new RuntimeException($payment->getErrorMessage());
    }

    // Persist before redirecting; the callback must be able to find this row.
    Order::where('invoice_number', 'INV-001')->update([
        'bkash_payment_id' => $payment->getPaymentId(),
        'payment_state' => 'initiated',
    ]);

    return redirect()->away($payment->getPaymentUrl());
} catch (BkashException $e) {
    report($e);
    return back()->withErrors('Payment could not be started.');
}

Then handle the callback by executing the payment and verifying the result:

Route::post('/bkash/callback', function (Request $request) {
    $payment = Bkash::executePayment($request->input('paymentID'));

    if ($payment->isCompleted()) {
        // Fulfil against your own order, not against callback-supplied amounts.
        event(new PaymentCompleted($payment));
        return redirect('/success');
    }

    return redirect('/failed');
})->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);

The first production concern is not the API call itself; it is making your own order state machine authoritative and using bKash responses only as confirmation.

Implementation Patterns

Keep order state in your app, not in bKash responses

Use bKash as an external payment state, but store your own pending, initiated, paid, failed, refunded states. Never fulfil an order solely because a callback arrived. Always execute or query the payment first.

$query = Bkash::queryPayment($order->bkash_payment_id);

if ($query->isCompleted() && $query->getAmount() === (float) $order->amount) {
    $order->markPaid($query->getTrxId());
}

For day-to-day code, wrap this in a small application service rather than calling the facade from controllers, jobs, and listeners in different ways:

final class BkashPaymentService
{
    public function start(Order $order): string
    {
        $payment = Bkash::createPayment(
            payerReference: $order->customer_reference,
            amount: (float) $order->amount,
            invoiceNumber: $order->invoice_number,
            callbackURL: route('bkash.callback'),
        );

        // Persist payment ID and return the redirect URL.
        $order->forceFill([
            'bkash_payment_id' => $payment->getPaymentId(),
            'payment_state' => 'initiated',
        ])->save();

        return $payment->getPaymentUrl();
    }

    public function confirm(Order $order, string $paymentId): bool
    {
        $result = Bkash::executePayment($paymentId);

        if (! $result->isCompleted()) {
            $order->markPaymentFailed($result->getErrorMessage());
            return false;
        }

        $order->markPaid($result->getTrxId());
        return true;
    }
}

Use tokenized checkout for repeat customers

The agreement flow is the reason to choose this package over a thin HTTP wrapper. Create an agreement once, store the agreementId, and pass it into later createPayment calls.

$agreement = Bkash::createAgreement($customer->reference);

if ($agreement->isSuccess()) {
    return redirect()->away($agreement->getAgreementUrl());
}

After the customer returns:

$agreement = Bkash::executeAgreement($request->input('paymentID'));

if ($agreement->isSuccess()) {
    $customer->update(['bkash_agreement_id' => $agreement->getAgreementId()]);
}

Then future payments:

$payment = Bkash::createPayment(
    payerReference: $customer->reference,
    amount: 250.00,
    invoiceNumber: $invoiceNumber,
    callbackURL: route('bkash.callback'),
    agreementId: $customer->bkash_agreement_id,
);

Treat agreementId as sensitive reusable payment authorization. Store it encrypted or at least guarded, and provide a cancel path:

Bkash::cancelAgreement($customer->bkash_agreement_id);

Use the helper for prototypes, not as your domain layer

BkashHelper is useful for quick flows and reference generation:

use SabitAhmad\Bkash\Helpers\BkashHelper;
use SabitAhmad\Bkash\Facades\Bkash;

$helper = new BkashHelper(app(Bkash::class));

$invoice = BkashHelper::generateInvoiceNumber('ORDER');
$payerRef = BkashHelper::generatePayerReference('CUSTOMER');

However, the constructor call shown in the README can be awkward because Bkash::class resolves through the facade. Prefer resolving the underlying service from the container or inject what the package actually binds. Check the service provider before relying on this pattern in production.

Use events for side effects, not as the source of truth

The package dispatches PaymentCompleted, PaymentFailed, and AgreementCreated. Use listeners for email, analytics, or fulfilment dispatch, but keep the order transition in the same database transaction as the callback handling where possible.

protected $listen = [
    \SabitAhmad\Bkash\Events\PaymentCompleted::class => [
        \App\Listeners\SendPaymentReceipt::class,
    ],
];

If a listener fails, the payment is still real. Do not let a mail failure roll back payment recording; queue listeners or wrap side effects.

Use query and search operations for reconciliation

The package exposes queryPayment, searchTransaction, refundPayment, and refundStatus. Build a scheduled reconciliation job rather than trusting only callbacks:

$result = Bkash::searchTransaction($order->bkash_trx_id);

foreach ($result->getTransactions() as $transaction) {
    if (($transaction['transactionStatus'] ?? null) === 'Completed') {
        // Compare with local state.
    }
}

For refunds, store the refund ID and reconcile separately:

$refund = Bkash::refundPayment($order->bkash_trx_id, 50.00, 'Partial return');

if ($refund->isSuccess()) {
    $order->refunds()->create([
        'refund_id' => $refund->getRefundId(),
        'amount' => $refund->getRefundAmount(),
        'status' => $refund->getTransactionStatus(),
    ]);
}

Standardize response handling

All responses share isSuccess(), getErrorMessage(), getStatusCode(), getRawData(), and toArray(). Build one small adapter so controllers do not branch on response class names:

function bkashError(\SabitAhmad\Bkash\Responses\BaseResponse $response): array
{
    return [
        'message' => $response->getErrorMessage(),
        'code' => $response->getErrorCode(),
        'status' => $response->getStatusCode(),
    ];
}

Log getRawData() on failure, but scrub credentials and tokens before writing to logs.

Gotchas and Tips

This is a low-usage package; pin and read the source

With one star and zero dependents, do not assume every documented method matches the installed version. Pin a known version in composer.json, and after upgrading, re-read the changelog and the service class. The README documents a broad API surface, but production behavior lives in the package source and the published config.

The API version is beta

Both base URLs point at v1.2.0-beta Tokenized Checkout endpoints. That is the current bKash product direction, but beta in the URL means endpoint behavior can change. Keep the bKash API version in config and avoid hard-coding URLs in your application.

Sandbox and production are not just credentials

Switching BKASH_SANDBOX changes the base URL. Test the complete redirect and callback flow against sandbox before changing credentials. Callback URLs must be reachable by the browser; a relative BKASH_CALLBACK_URL=/bkash/callback is fine only if your route resolves it correctly and bKash's redirect uses the expected host.

Exclude the callback route from CSRF protection

bKash posts back to your callback URL without your Laravel session token. Register the route outside the web middleware group or exclude it from VerifyCsrfToken. If you use auth middleware on the callback, the session may be missing; identify the order by paymentID instead.

Do not trust callback parameters

The callback can include paymentID, status, and other values. Use paymentID only as a lookup key, then execute or query the payment server-side. Validate that the amount, invoice number, and currency match your order before marking it paid.

Persist before redirecting

A common failure is creating the payment, redirecting, and only persisting the paymentID afterward. The callback may arrive before the redirect response is processed, or the user may close the browser. Write the payment ID and state before returning the redirect.

Make callbacks idempotent

bKash may retry callbacks, users may refresh, and your own retry logic may execute the same payment twice. Use a unique constraint or atomic update on the payment ID and only transition initiated -> paid once. Wrap fulfilment in a transaction or an idempotency key.

DB::transaction(function () use ($paymentId, $result) {
    $order = Order::where('bkash_payment_id', $paymentId)->lockForUpdate()->firstOrFail();

    if ($order->payment_state === 'paid') {
        return;
    }

    $order->markPaid($result->getTrxId());
});

Use invoice numbers and payer references carefully

invoiceNumber should be unique per payment attempt. Reusing an invoice number can confuse reconciliation and may be rejected by bKash. payerReference is not a secure identifier; treat it as a customer reference, not an authorization token.

Token caching and retries are configurable, but not infinitely resilient

BKASH_TOKEN_CACHE_TTL=3300, BKASH_TIMEOUT, BKASH_RETRY_ATTEMPTS, and BKASH_RETRY_DELAY are exposed. If your queue workers make many bKash calls, keep token cache TTL below the token expiry and avoid retrying non-idempotent operations blindly. Creating a payment twice because of a timeout can produce two payment IDs; reconcile with queryPayment and your own order state.

Validation is configurable — use it deliberately

BKASH_STRICT_VALIDATION, BKASH_MAX_AMOUNT, and BKASH_MIN_AMOUNT affect request validation. Keep strict validation on outside local development. Amount handling should use your application's money representation, not floating-point arithmetic, even though the response methods expose ?float.

Transaction logging may log sensitive data

BKASH_LOG_TRANSACTIONS=true writes transaction records. Review the migration and model before enabling it in production, especially if you store raw request/response payloads. Consider retention and access controls, and never log app secrets or bearer tokens.

The helper's constructor is a signal to check the container binding

new BkashHelper(app(Bkash::class)) looks suspicious because Bkash is documented as a facade. If it fails, inspect the package service provider and resolve the actual client class or use the facade directly. Do not build critical flows around an undocumented constructor shape.

Refunds need their own reconciliation

A successful refund response does not always mean the money has settled. Store getRefundId(), getTransactionStatus(), and getRefundTime(), and poll refundStatus or use search for final reconciliation. Partial refunds should be tracked per refund ID, not as a single order-level flag.

Use the raw response for edge cases

The normalized response classes expose many convenience methods, but bKash may add fields. getRawData() and toArray() are your escape hatch. In logs and support tickets, include the raw response plus your local payment ID, invoice number, and environment.

Watch the events' timing

The package events are dispatched by package code, not by your application. Confirm which events fire on which operation, and whether they fire on failed execution as well as successful completion. Keep your own state transition authoritative and use events only for notifications or downstream work.

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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo