• Open source
  • BDPayGate
  • Laravel
  • Payments

BDPayGate: one Laravel API for every Bangladeshi payment gateway

BDPayGate is our open-source Laravel package for bKash, Nagad, SSLCommerz and nine more gateways, with every payment verified server-side before an order is paid.

If you sell online in Bangladesh, you probably need more than one payment gateway. Customers expect bKash and Nagad; cards and bank transfers usually come through SSLCommerz, ShurjoPay or aamarPay. Each gateway has its own API, its own callback format and its own quirks — and every integration has to get the same hard part right: never mark an order paid unless the money really moved.

BDPayGate is our answer. It is an open-source Laravel package, released under the MIT license, that puts every Bangladeshi payment gateway behind one API.

composer require byteminus/laravel-bdpaygate

Documentation lives at bdpaygate.github.io, the code at github.com/byteminus/laravel-bdpaygate and the package on Packagist.

Twelve gateways, one interface

BDPayGate supports bKash, Nagad, SSLCommerz, Upay, ShurjoPay, aamarPay, PortPos (formerly PortWallet), PayStation, UddoktaPay, Moneybag, EPS and DGePay. Rocket, OK Wallet and CellFin have no public merchant API of their own, so you accept them through SSLCommerz, ShurjoPay or aamarPay.

Every gateway uses the same four operations — charge, callback, sync and refund — so switching gateways, or offering several at checkout, doesn't mean writing a new integration each time. Seven of them (bKash, SSLCommerz, ShurjoPay, aamarPay, PayStation, UddoktaPay and Moneybag) have been run end to end on the gateways' real sandboxes, and the package ships with 401 tests. It supports Laravel 11 to 13 on PHP 8.2 to 8.5.

The whole integration

Taking a payment is a few lines:

use ByteMinus\BdPayGate\Facades\BdPayGate;
use ByteMinus\BdPayGate\Data\Customer;

$payment = BdPayGate::driver('bkash')->charge(
    amount: 1500,
    reference: 'ORD-1001',
    customer: new Customer(name: 'Rahim Uddin', phone: '01711000000'),
);

return redirect($payment->checkout_url);

Fulfilment happens in an event listener, never on the thank-you page:

use ByteMinus\BdPayGate\Events\PaymentCompleted;

// Fires once, only after the gateway confirmed the payment.
Event::listen(PaymentCompleted::class, function (PaymentCompleted $event) {
    Order::where('number', $event->payment->reference)
        ->first()
        ->markPaid($event->payment->gateway_transaction_id);
});

Built for the payment that goes wrong

Most integrations work on the happy path. Money goes missing on the other ones, so that is where BDPayGate spends most of its code.

No callback is trusted

A redirect, an IPN or a webhook is only a claim that something happened. Anyone can send your callback URL a request that says "success". BDPayGate uses the callback only to learn which payment to check, then asks the gateway's own API — server to server — and requires the answer to name this exact attempt, with a matching amount and currency. Only then does the payment change state.

The IPN and the customer's browser redirect often arrive at the same moment. Status changes are atomic, so PaymentCompleted fires once, and you ship the order once.

A timeout is not a failure

If the gateway doesn't answer, the payment stays pending rather than being marked failed. Running php artisan bdpaygate:sync settles it later, including payments whose callback never arrived because the customer closed the browser. A customer who paid is never told they didn't.

Retries can't double charge

Every retry is a new attempt with its own gateway ID. If a customer manages to pay for two attempts, you get a PaymentDuplicated event to refund — not a second order.

Failed can still become paid

Sometimes a gateway reports a failure and later confirms the money moved after all. BDPayGate completes the payment when that happens, and a forged "cancelled" message can't strand an order that was actually paid.

Customers stay signed in

Gateways send customers back with a cross-site POST, which wipes Laravel's session cookie and logs people out at the worst possible moment. BDPayGate's callback routes run without sessions, so nobody loses their login at checkout.

Refunds

Where a gateway offers a refund API, BDPayGate exposes it with the same interface, including partial refunds:

$result = BdPayGate::refund($payment, 500, 'Damaged item'); // partial
$result = BdPayGate::refund($payment);                       // everything left

$result->accepted; // the gateway took it
$result->pending;  // it settles later at the bank

bKash supports full and partial refunds (up to 10 per transaction), and SSLCommerz, Upay and PortPos have refund APIs too. The remaining gateways only allow refunds from their merchant panels, and the documentation says so for each one.

Testing without real money

BdPayGate::fake() replaces every gateway with an in-memory fake: no network and no credentials.

$fake = BdPayGate::fake();

$payment = BdPayGate::charge(300, 'ORD-1', $customer);
$this->get($payment->checkout_url); // completes it like a real redirect
$fake->fail($payment);              // or drive it from code

For a real end-to-end check, point a local app at the gateway's sandbox keys.

Why we open-sourced it

Payment code is the code you most want other people to read. Every merchant in Bangladesh solves the same problems against the same gateways, and most of those solutions are copied from old blog posts and never verify a callback. A shared, tested package means the hard cases are handled once, in the open, where anyone can check the work.

It follows the rule we hold to in all our own software: the gateway's own answer is the truth, never the browser.

Get started

Found a gateway quirk we haven't handled? Open an issue. Security problems, please report privately to contact@byteminus.com.

Keep reading

Eleven open-source Go packages for SaaS backends

Tenancy, permissions, approvals, audit logs, settings, feature flags, translations, media, notifications, backups and an admin panel, as MIT-licensed Go modules.

  • Open source
  • Go
  • PostgreSQL
  • Multi-tenant SaaS

Follow what we build

We release the parts of our products that are useful to others as MIT-licensed open source. Follow along on GitHub, or subscribe to the blog.