BDPayments is a small Node.js library that talks to Stripe,
SSLCommerz, bKash and Nagad using the exact same
instructions. Learn it once, and switching or adding a payment provider becomes a one-word change.
0 gateways Node 18+ No required dependencies TypeScript included0 tests
Never done this before? Start here.
Skip ahead if you already integrate payment gateways for a living.
The travel-adapter analogy. Every country has a different wall socket.
Rather than buying a new charger for each one, you carry a single universal adapter.
Payment providers are the sockets. Each one speaks its own language, needs its own
passwords, and returns answers in its own shape. BDPayments is the adapter that lets your
code plug into any of them the same way.
Some words you'll see everywhere
Word
What it actually means
Gateway
A company that moves money for you — Stripe, bKash, Nagad, SSLCommerz.
Charge
Asking for money from a customer.
Refund
Giving some or all of it back.
Transaction ID
The receipt number. Keep it — you need it to refund or check a payment later.
Credentials
Your secret keys. They prove the money should go to your bank account.
Sandbox
A pretend mode for testing. Fake money, real behaviour. Always start here.
Callback / IPN
The gateway phoning your server back to say "that payment went through".
The one rule that matters most: never treat an order as paid just because
the customer's browser came back to your success page. Anyone can type that address.
Always ask the gateway directly with retrieve(). This page will remind you
again where it counts.
Everything is one of four actions
The entire library is these four verbs. Every gateway uses the same ones.
charge()
Ask the customer for money. Returns a receipt, or a link to send them to.
execute()
Finish a payment the customer already approved. bKash only.
refund()
Send money back — all of it, or part.
retrieve()
Ask the gateway "did this actually happen?". Your source of truth.
Every one of them answers in the same shape, no matter which gateway you used:
{
success: true, // did it work?
transactionId: 'pi_3Nk...', // the receipt number — save this
status: 'succeeded', // the gateway's own wording
amount: 1000,
currency: 'usd',
gatewayResponse: { ... } // the untouched raw reply, if you need it
}
Install
npm install bdpayments
That's everything for SSLCommerz, bKash and Nagad — they need no extra software.
Only add Stripe's own package if you use Stripe:
npm install stripe # only if you use Stripe
Forget it and you won't get a confusing crash — you'll get a plain message saying
The "stripe" peer dependency is not installed. Run: npm install stripe.
The one decision to make first
Before writing any code, work out which of these describes you. It changes
where your secret keys live — and almost nothing else.
Mode A
Single company
You run one business. Money from every customer lands in
your bank account.
One online shop or restaurant
One school collecting fees
One SaaS billing its own subscribers
Set your keys once at startup and never think about
them again. → Show me
Mode B
Multi-tenant (SaaS / marketplace)
You built a platform. Each business on it has its own
merchant account, and money goes to them, not you.
A shop-builder hosting 500 stores
A food-delivery app paying restaurants
A booking system for many clinics
Pass that tenant's keys with each payment. →
Show me
You can do both at once. Set your own keys globally as a fallback, and
override them per payment for tenants who have brought their own. That is exactly how the
priority order in How keys resolve is designed to work.
Side by side
Single company
Multi-tenant
Who gets the money
You
Each tenant
How many key sets
One
One per tenant
Where keys live
Env vars or configure()
Your database, encrypted
How you pass them
Once at startup
On every call
Setup effort
Two minutes
An afternoon — you also need onboarding + encrypted storage
Mode A — Single company
One business, one set of keys. Tell BDPayments about them once when your app
boots.
Where the money goes
Customer 1Your account
Customer 2Your account
Customer 3Your account
Every payment ends up in the same
place, so one set of keys covers everything.
Put your keys in environment variables
Never type secret keys directly into your code — anyone who reads your repository would see
them. Keep them in a .env file that you never commit.
# .env — add this file to .gitignore!
STRIPE_API_KEY=sk_test_...
BKASH_APP_KEY=...
BKASH_APP_SECRET=...
BKASH_USERNAME=...
BKASH_PASSWORD=...
BKASH_SANDBOX=true
That alone is enough — BDPayments reads
these automatically. The next step is only if you prefer setting them in code.
Or call configure() once at startup
Useful when your keys come from a secrets manager rather than the environment.
From now on every call finds the credentials by itself.
import { charge } from 'bdpayments';
const payment = await charge({
gateway: 'bkash',
amount: 500,
invoiceNumber: 'INV-001',
callbackURL: 'https://myshop.com/bkash/callback',
});
// Send the customer here to approve the payment
console.log(payment.bkashURL);
A typo like configure({ strip: … }) is rejected immediately with a clear
message, instead of quietly failing later with "missing credentials".
Mode B — Multi-tenant SaaS
Many businesses on one platform, each with their own merchant account. Money
must reach the right one.
What "multi-tenant" means. Imagine you built software that lets anyone open
an online shop. Five hundred shops now run on it. When a customer buys from
Rahim's Bakery, that money must land in Rahim's bKash account — not yours, and
certainly not the bakery next door's.
So there is no single "the" key. Each shop has its own, and you pick the right one for every
single payment.
Where the money goes
Order at Rahim'sRahim's bKash
Order at Karim'sKarim's bKash
Order at Fatima'sFatima's bKash
Three separate destinations, so
three separate sets of keys — and the lanes must never cross.
Collect each tenant's keys when they sign up
Rahim opens his own bKash merchant account and pastes those keys into your dashboard. Store
them encrypted — they are as sensitive as passwords.
Speeding things up must never mean mixing two tenants' money. Here is what the library does about
that, so you don't have to:
Login sessions kept separate
bKash requires a login token before each action. Tokens are cached per tenant, so Rahim's
token can never be used to charge on Karim's account.
Stripe clients kept separate
One reusable connection per API key. Two tenants never share one.
Only real keys are read
A field named amount or currency can never be mistaken for a
credential. Only the documented key names are ever picked up.
Secrets hidden from logs
Printing a config object shows [redacted] instead of a live key, so tenant
secrets don't leak into your logging service.
Your responsibilities, not the library's.
Encrypt tenant credentials at rest. Never log them. Check that the logged-in user really
owns the tenant before charging on their behalf — the library will faithfully use whatever
keys you hand it, and cannot tell a legitimate lookup from a mixed-up one.
How keys are found
Three places, checked in order. The first one that has a key wins — which is
what lets a tenant override your defaults.
STRIPE_API_KEY and friends. The fallback when nothing else is set.
So a platform can keep its own keys as the default and still let individual tenants bring theirs:
// Platform default, set once at startup
configure({ stripe: { apiKey: process.env.PLATFORM_STRIPE_KEY } });
// Tenants without their own account use the platform's
await charge({ gateway: 'stripe', amount: 1000, currency: 'usd' });
// Tenants with their own account override it, just for this call
await charge({ gateway: 'stripe', apiKey: tenant.stripeKey,
amount: 1000, currency: 'usd' });
Only credential names are read from a call. Recognised names —
apiKey, storeId, storePassword, appKey,
appSecret, username, password,
merchantId, publicKey, privateKey,
sandbox, timeoutMs — plus clientIp and
baseUrl for Nagad. Everything else is treated as payment data.
If a required key is missing you get told exactly which one, before any network call:
ConfigurationError: Missing required credentials for "bkash": appSecret, password.
Provide them via configure(), per-call options, or environment variables.
The payment journey
What actually happens between "customer clicks Pay" and "order marked paid".
Redirect gateways — SSLCommerz & Nagad
The customer leaves your site to pay, then comes back.
Customer
Clicks Pay
On your checkout page
Your server
charge()
Gets back a payment link
Gateway
Customer pays
On the gateway's own page
Gateway
Sends them back
Plus a callback to your server
Your server
retrieve()
Confirm, then mark paid
Step 5 is not optional. A returning browser proves nothing — a dishonest
customer can open your success page without paying a taka. Only what the gateway tells your
server directly, via retrieve(), counts as payment.
bKash — approve, then finish
bKash splits it into two calls: you create the payment, the customer approves it in the bKash app,
then you complete it.
Your server
charge()
Returns bkashURL
Customer
Approves
Enters their bKash PIN
Your server
execute()
Money actually moves here
You
Save trxID
Needed for refunds
Skip execute() and no money moves — the payment simply expires. If you also
want to refund it later, you must store the trxID it returns.
Stripe — one step
With a saved card, a single charge() completes the payment. No redirect, no second
call.
The four gateways
Each one loads only when you actually use it, so unused gateways cost you
nothing.
Stripe
International cards. Best for customers paying from outside Bangladesh.
API keyCards
SSLCommerz
Bangladesh's biggest aggregator — cards, mobile wallets and net banking in one checkout.
Store IDRedirect
bKash
The most widely used mobile wallet in Bangladesh. Two-step tokenized checkout.
TokenWallet
Nagad
Government-backed mobile wallet. Uses an encrypted key pair rather than a password.
const payment = await charge({
gateway: 'nagad',
amount: 500, // 500 BDT
orderId: 'ORD-001', // your own order reference
callbackURL: 'https://myshop.com/nagad/callback',
});
redirect(payment.callBackUrl);
Nagad signs and encrypts every request with
your key pair. Paste the PEM keys in exactly as issued.
Execute — finishing a bKash payment
Only bKash needs this. It is the step where money actually moves.
import { execute } from 'bdpayments';
const result = await execute({
gateway: 'bkash',
paymentID: 'TR0011...', // from charge()
});
result.status; // "Completed"
result.trxID; // save this — you need it to refund
Calling execute() on a gateway that doesn't support it fails clearly with
UNSUPPORTED_OPERATION rather than doing something unexpected.
Refund — giving money back
import { refund } from 'bdpayments';
// Full refund
await refund({ gateway: 'stripe', transactionId: 'pi_...' });
// Partial refund — $5.00 of a larger payment
await refund({ gateway: 'stripe', transactionId: 'pi_...', amount: 500 });
// bKash needs both IDs: the payment's and the transaction's
await refund({
gateway: 'bkash',
transactionId: 'TR0011...', // paymentID from charge()
trxID: 'BGH7DS8...', // trxID from execute()
amount: 200,
reason: 'Customer requested',
});
Always check result.success. Some gateways accept a refund
request and process it later. In that case you get success: false with the
real state in result.status (for example "processing") — the
money has not moved yet.
A refund the gateway rejected throws an error instead, so it can never be mistaken
for a successful one.
A timeout throws TIMEOUT — but the payment may still have succeeded.
Don't retry blindly. Call retrieve() to find out what really happened, or
retry with the same idempotency key.
Webhooks & IPN
The gateway phoning your server directly to report a payment. More reliable
than the customer's browser — but you must check the call is genuine.
import { verifySslcommerzIpn, retrieve } from 'bdpayments';
app.post('/ipn/sslcommerz', async (req, res) => {
// 1. Is this really from SSLCommerz?
if (!verifySslcommerzIpn(req.body)) return res.sendStatus(400);
// 2. Confirm the status with SSLCommerz itself
const payment = await retrieve({
gateway: 'sslcommerz',
valId: req.body.val_id,
});
// 3. Only now is it safe to fulfil the order
if (payment.success) await markOrderPaid(payment.transactionId);
res.sendStatus(200);
});
Nagad callbacks carry no signature.parseNagadCallback()
tidies one up for you, but always reports verified: false — that is honest,
not a bug. Confirm with retrieve() before trusting it.
Environment variables or an encrypted database. Never committed to git.
Always confirm server-side
A returning browser is not proof. retrieve() is.
Trust your own prices
Work out the amount on your server from your database — never from what the browser sent.
Sandbox first
Test every path — success, failure, cancel, refund — before switching
sandbox off.
Nagad on certain Node.js versions. Node 18.19.1, 20.11.1 and 21.6.2
disabled a decryption method Nagad depends on. On those exact versions Nagad payments fail
with a message saying so. Upgrade to a newer patch release.
API reference
Function
Description
Returns
configure(configs)
Set app-wide credentials for one or more gateways.
void
clearConfig()
Forget all stored credentials and cached sessions.
void
charge(options)
Ask a customer for money.
Promise<PaymentResult>
execute(options)
Finish an approved bKash payment.
Promise<PaymentResult>
refund(options)
Return money to a customer.
Promise<RefundResult>
retrieve(options)
Check a payment's real status.
Promise<RetrieveResult>
getSupportedGateways()
List gateway names.
string[]
getGatewayCapabilities(name)
Which actions a gateway supports.
Promise<GatewayCapabilities>
verifySslcommerzIpn(payload)
Check an SSLCommerz IPN is genuine.
boolean
parseNagadCallback(query)
Tidy a Nagad callback (always unverified).
NagadCallback
Environment variables
The lowest-priority way to supply credentials — ideal for single-company
setups.