Laravel payment gateway for Bangladesh
Every Bangladeshi payment gateway. One Laravel API.
Accept bKash, Nagad, SSLCommerz, Upay, ShurjoPay, aamarPay, PortPos, PayStation, UddoktaPay, Moneybag, EPS and DGePay with the same three lines of code. Every payment is confirmed with the gateway before your order is marked paid.
12 gateways7 proven on real sandboxes401 testsLaravel 11–13 · PHP 8.2–8.5MIT licensed
- charge created, customer sent to bKash
- callback says "success" · not trusted yet
- signature matches the one bKash issued
- execute → bKash: Completed, trxID DJF00PPRDK
- amount ৳120.00 = ৳120.00, currency BDT
- completed · PaymentCompleted fired once
Built for the payment that goes wrong
Most integrations work on the happy path. Money goes missing on the other ones.
No callback is trusted
Redirects, IPNs and webhooks are claims. The package asks the gateway's own API, checks the amount and currency, and only then changes the payment.
Paid exactly once
The IPN and the customer's redirect often arrive together. Status changes are atomic, so PaymentCompleted fires once.
A timeout is not a failure
If the gateway doesn't answer, the payment stays pending and bdpaygate:sync settles it later. A customer who paid is never marked failed.
Retries can't double charge
Each retry is a new attempt with its own gateway id. If a customer pays two of them, you get PaymentDuplicated to refund, not a second order.
Failed can still become paid
When a gateway confirms the money moved after a failure was reported, the payment completes. A fake "cancelled" can't strand a paid order.
Customers stay signed in
Gateways return customers with a cross-site POST that wipes Laravel sessions. The callback routes run without sessions, so nobody gets logged out at checkout.
12 gateways, one interface
bKash, Nagad, SSLCommerz and the rest all use the same charge, callback, sync and refund.
| Gateway | Status | Refunds | Sandbox run |
|---|---|---|---|
| bKashTokenized Checkout V2, v1.2 | Stable | Full and partial | Paid, refunded twice, forged callback rejected |
| SSLCommerzCards, banks, wallets | Stable | Full and partial | Paid, risk flag, partial refund |
| ShurjoPayCards, wallets, banks | Stable | Merchant panel | Paid and declined |
| aamarPayCards, wallets, banks | Stable | Merchant panel | Paid and declined |
| PayStationCards, wallets, banks | Stable | Merchant panel | Paid via bKash |
| MoneybagCards, wallets | Stable | Merchant panel | Paid and failed |
| UddoktaPaySelf-hosted | Stable | Merchant panel | Paid, forged invoice rejected |
| NagadMobile wallet | Beta | Merchant panel | Needs merchant onboarding |
| UpayMobile wallet | Beta | Full | Needs merchant onboarding |
| PortPosFormerly PortWallet | Beta | Full and partial | Self-serve sandbox |
| EPSEasy Payment System | Experimental | Merchant panel | Docs on request |
| DGePayAggregator | Experimental | Merchant panel | Docs on request |
Rocket, OK Wallet and CellFin have no public merchant API of their own. Accept them through SSLCommerz, ShurjoPay or aamarPay. "Merchant panel" means the gateway publishes no refund API, so refunds are made from its dashboard.
What happens to every payment
The same steps for all 12 gateways. Nothing the browser sends can mark an order paid.
You call
charge()The package records a pending attempt with a fresh gateway id and returns the checkout URL to redirect to.
The customer pays on the gateway's page
bKash, Nagad, a card or a bank. Your app does nothing here.
The gateway calls back, and is not believed
The redirect or IPN only says which payment to check. Its status, amount and ids are ignored.
The package asks the gateway's own API
Validation, execute or status query, server to server. The answer must name this attempt, and the amount and currency must match.
One atomic update, one event
PaymentCompleted,PaymentFailedorPaymentCancelledfires once. If the gateway can't be reached, the payment stays pending forbdpaygate:sync.
The whole integration
Install, add your keys to .env, and write these.
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);
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);
});
$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
// No network, 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
Questions developers ask
Which Bangladeshi payment gateways does it support?
bKash, Nagad, SSLCommerz, Upay, ShurjoPay, aamarPay, PortPos (formerly PortWallet), PayStation, UddoktaPay, Moneybag, EPS and DGePay. Rocket, OK Wallet and CellFin work through SSLCommerz, ShurjoPay or aamarPay.
Which Laravel and PHP versions are supported?
Laravel 11, 12 and 13 on PHP 8.2 to 8.5. Laravel 13 needs PHP 8.3 or newer.
Can I refund a bKash payment from Laravel?
Yes. bKash supports full and partial refunds, up to 10 per transaction, and the package tracks the refunded amount. SSLCommerz, Upay and PortPos also have refund APIs. The other gateways only allow refunds from their merchant panel.
What happens if the customer closes the browser after paying?
The gateway's IPN still completes the payment, and bdpaygate:sync settles any payment whose callback never arrived. Do your fulfilment in a PaymentCompleted listener, not on the thank-you page.
Can I use two SSLCommerz stores in one app?
Yes. Each entry in the config is a separate gateway name with its own credentials, routes and payments, so two stores of the same gateway work side by side.
How do I test without real payments?
Use BdPayGate::fake() in your tests: no network and no credentials. For a real end-to-end check, use the gateway's sandbox keys in a local app.
Is it free?
Yes. BDPayGate is open source under the MIT license.