# 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.

- URL: https://byteminus.com/blog/bdpaygate-laravel-payment-gateway-bangladesh/
- Published: 2026-10-15
- Author: Byte Minus Engineering (Byte Minus)
- Topics: Open source, BDPayGate, Laravel, Payments

**In short:** BDPayGate (byteminus/laravel-bdpaygate) is a free, MIT-licensed Laravel package by Byte Minus that supports bKash, Nagad, SSLCommerz, Upay, ShurjoPay, aamarPay, PortPos, PayStation, UddoktaPay, Moneybag, EPS and DGePay through one API. It confirms every payment with the gateway server to server, marks each payment paid exactly once, and supports Laravel 11 to 13 on PHP 8.2 to 8.5.

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](https://bdpaygate.github.io/), the code at [github.com/byteminus/laravel-bdpaygate](https://github.com/byteminus/laravel-bdpaygate) and the package on [Packagist](https://packagist.org/packages/byteminus/laravel-bdpaygate).

## 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:

```php
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:

```php
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.

### Paid exactly once

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:

```php
$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.

```php
$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

- Install: `composer require byteminus/laravel-bdpaygate` ([Packagist](https://packagist.org/packages/byteminus/laravel-bdpaygate))
- Docs, gateway setup guides and `.env` keys: [bdpaygate.github.io](https://bdpaygate.github.io/)
- Source, issues and pull requests: [github.com/byteminus/laravel-bdpaygate](https://github.com/byteminus/laravel-bdpaygate)

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