Home / Node.js / SSLCommerz Stable
SSLCommerz
Also for Laravel: https://github.com/byteminus/laravel-bdpaygate/blob/main/docs/gateways/sslcommerz.md
Status: stable · Driver: sslcommerz · Refunds: full and partial (asynchronous) · Sync: yes
SSLCommerz is a hosted checkout covering cards, internet banking and most mobile wallets. It is also how you accept wallets that have no public merchant API of their own: Rocket (DBBL), OK Wallet and IBBL / CellFin are offered on the SSLCommerz checkout page.
Setup
SSLCOMMERZ_STORE_ID=yourstore
SSLCOMMERZ_STORE_PASSWORD=yourstore@ssl
SSLCOMMERZ_SANDBOX=true
These are read by gatewaysFromEnv(process.env). Or configure the gateway in code:
gateways: {
sslcommerz: {
storeId: process.env.SSLCOMMERZ_STORE_ID,
storePassword: process.env.SSLCOMMERZ_STORE_PASSWORD,
sandbox: true,
},
}
sandbox defaults to true. Set it to false for live payments. Hosts are https://sandbox.sslcommerz.com (sandbox) and https://securepay.sslcommerz.com (live).
Sandbox stores are self-serve at https://developer.sslcommerz.com/registration/. Live refunds need your server's public IP registered with SSLCommerz. If your app has no fixed outbound IP, pass a fetch that goes through a proxy with one:
import { fetch, ProxyAgent } from 'undici';
const dispatcher = new ProxyAgent(process.env.FIXED_IP_PROXY!);
gateways: {
sslcommerz: { /* … */, fetch: (url, init) => fetch(url, { ...init, dispatcher }) },
}
Take a payment
const payment = await pay.charge({
gateway: 'sslcommerz',
amount: 1500,
reference: 'ORD-1001',
customer: { name: 'Rahim Uddin', phone: '01711000000', email: 'rahim@example.com', address: 'Dhanmondi' },
description: 'Order ORD-1001', // sent as product_name
meta: { category: 'electronics' }, // meta.category and meta.profile fill product_category and product_profile
});
return redirect(payment.checkoutUrl);
How a payment is verified
pay.charge()creates a session (/gwprocess/v4/api.php) with this attempt'sgatewayReferenceastran_id, the payment id invalue_aand your own reference invalue_b.- SSLCommerz posts the IPN and redirects the customer (
/bdpaygate/sslcommerz/{success|fail|cancel|ipn}). - A success is accepted only if the
verify_signsignature matches and the Order Validation API confirms theval_id, thetran_idand the amount. For foreign-currency payments,currency_typeandcurrency_amountare checked rather than the convertedamount. - A fail or cancel is a claim anyone could send: the package asks the Transaction Query API first, and completes the payment if SSLCommerz says it was paid.
risk_level = 1completes the payment but setspayment.requiresReview = true. SSLCommerz asks you to hold the service and verify the customer before delivering.
bank_tran_id is stored in payment.gatewayTransactionId.
pay.on('payment.completed', async ({ payment }) => {
if (payment.requiresReview) {
// risk_level=1: hold delivery until you have checked the customer.
return;
}
await orders.markPaid(payment.reference, payment.gatewayTransactionId);
});
pay.sync(payment) and pay.syncPending() use the Transaction Query API. A payment SSLCommerz reports as paid completes; otherwise it stays as it is.
Refunds
import type { SslCommerzGateway } from 'bdpaygate';
const result = await pay.refund(payment, 500, 'Damaged item');
result.accepted; // true: SSLCommerz initiated it
result.pending; // true: the bank settles it later
result.refundId; // refund_ref_id
await (pay.for(payment) as SslCommerzGateway).refundStatus(result.refundId!); // "refunded", "processing" or "cancelled"
A processing answer to a refund request means that refund was already initiated, so it is not recorded a second time. An accepted refund fires refund.processed and updates payment.refundedAmount.
Limits and quirks
- Amount must be 10.00 to 500000.00 BDT. Checked before the API call.
tran_idis at most 30 characters; attempt ids are 20.- The return to your site is a cross-site POST. Mount the bdpaygate handler before any CSRF, session or auth middleware so the POST is accepted and customers are not logged out. Your landing page receives a normal GET.
- SvelteKit and Astro reject cross-site form POSTs by default. Add
https://sandbox.sslcommerz.comandhttps://securepay.sslcommerz.comto SvelteKit'skit.csrf.trustedOrigins, or allow them in Astro'ssecurity.checkOriginhandling.
Sandbox runs
This Node package was run end to end in the SSLCommerz sandbox on 2026-10-15:
- A payment was paid via Rocket, and the cross-site POST return was handled.
- Sync of the paid payment worked.
- A partial refund was accepted asynchronously, and
refundStatus()reported"processing". - Syncing an unpaid payment left it pending.