A unified payment gateway package for Node.js. One API for multiple payment providers.
Supports Stripe, SSLCommerz, bKash, and Nagad through a single, consistent interface.
npm install bdpaymentsThen install only the gateway SDKs you need:
# For Stripe
npm install stripe
# SSLCommerz, bKash and Nagad need no extra SDK β they use the REST API via fetchimport { configure } from 'bdpayments';
configure({
stripe: {
apiKey: process.env.STRIPE_API_KEY,
},
bkash: {
appKey: process.env.BKASH_APP_KEY,
appSecret: process.env.BKASH_APP_SECRET,
username: process.env.BKASH_USERNAME,
password: process.env.BKASH_PASSWORD,
sandbox: true,
},
});Or skip configure() entirely and set environment variables β the package reads them automatically.
import { charge } from 'bdpayments';
// Stripe
const stripeResult = await charge({
gateway: 'stripe',
amount: 1000, // $10.00 in cents
currency: 'usd',
paymentMethod: 'pm_card_visa',
confirm: true,
});
// bKash
const bkashResult = await charge({
gateway: 'bkash',
amount: 500,
invoiceNumber: 'INV-001',
callbackURL: 'https://example.com/callback',
});
// SSLCommerz
const sslResult = await charge({
gateway: 'sslcommerz',
amount: 1000,
currency: 'BDT',
transactionId: 'TXN-001',
successUrl: 'https://example.com/success',
failUrl: 'https://example.com/fail',
cancelUrl: 'https://example.com/cancel',
});import { refund } from 'bdpayments';
const result = await refund({
gateway: 'stripe',
transactionId: 'pi_...',
amount: 500, // Partial refund
});Check
result.success. A refund that the gateway accepts but has not completed (SSLCommerzprocessing, for example) returnssuccess: falsewith the real status inresult.status. A refund the gateway rejected throws aPaymentError.
import { retrieve } from 'bdpayments';
// Retrieve Stripe payment details
const stripeResult = await retrieve({
gateway: 'stripe',
transactionId: 'pi_...',
});
// Validate SSLCommerz payment callback or IPN using validation ID
const sslcommerzResult = await retrieve({
gateway: 'sslcommerz',
valId: 'val_id_from_callback',
});
console.log(stripeResult.status); // e.g. "succeeded"
console.log(sslcommerzResult.success); // true if validated successfullyimport { execute } from 'bdpayments';
// bKash (execute tokenized payment)
const result = await execute({
gateway: 'bkash',
paymentID: 'TR0011...',
});
console.log(result.status); // e.g. "Completed"Three ways to provide API keys (highest priority wins):
| Priority | Method | Example |
|---|---|---|
| 1 (highest) | Per-call options | charge({ gateway: 'stripe', apiKey: 'sk_...', ... }) |
| 2 | Global configure() |
configure({ stripe: { apiKey: 'sk_...' } }) |
| 3 (lowest) | Environment variables | STRIPE_API_KEY=sk_... |
Only recognised credential and transport keys are read from per-call options β business fields
such as amount never end up in the credential object. Resolved credentials also redact
themselves when logged or JSON.stringify-ed.
| Gateway | Variables |
|---|---|
| Stripe | STRIPE_API_KEY |
| SSLCommerz | SSLCOMMERZ_STORE_ID, SSLCOMMERZ_STORE_PASSWORD, SSLCOMMERZ_SANDBOX |
| bKash | BKASH_APP_KEY, BKASH_APP_SECRET, BKASH_USERNAME, BKASH_PASSWORD, BKASH_SANDBOX |
| Nagad | NAGAD_MERCHANT_ID, NAGAD_PUBLIC_KEY, NAGAD_PRIVATE_KEY, NAGAD_SANDBOX, NAGAD_CLIENT_IP |
| Gateway | Charge | Execute | Refund | Retrieve | Auth Method |
|---|---|---|---|---|---|
| Stripe | β PaymentIntents | β | β | β | API Key |
| SSLCommerz | β Session | β | β | β | Store ID/Password |
| bKash | β Tokenized | β | β | β | Token Grant |
| Nagad | β Checkout | β | β | β | RSA Encrypted |
Query this at runtime with getGatewayCapabilities('bkash').
Retrying a charge is only safe with an idempotency key. The mechanism differs per gateway:
| Gateway | Key | How |
|---|---|---|
| Stripe | idempotencyKey option |
Sent as Stripe's Idempotency-Key header |
| SSLCommerz | transactionId (tran_id) |
Natural key β reuse it when retrying |
| bKash | invoiceNumber |
Natural key β reuse it when retrying |
| Nagad | orderId |
Natural key β reuse it when retrying |
await charge({
gateway: 'stripe',
amount: 1000,
currency: 'usd',
idempotencyKey: `order-${orderId}`,
});Requests time out after 30 seconds by default (configure({ stripe: { timeoutMs: 10000 } }) to
change it). A timeout throws PaymentError with code TIMEOUT β the payment may still have
succeeded, so confirm with retrieve() rather than blindly retrying without a key.
import { verifySslcommerzIpn, retrieve } from 'bdpayments';
app.post('/ipn/sslcommerz', async (req, res) => {
if (!verifySslcommerzIpn(req.body)) return res.sendStatus(400);
// Signature proves the payload is authentic; retrieve() confirms the status.
const payment = await retrieve({ gateway: 'sslcommerz', valId: req.body.val_id });
if (payment.success) await markOrderPaid(payment.transactionId);
res.sendStatus(200);
});Nagad callbacks carry no signature, so parseNagadCallback() normalizes them but always
reports verified: false β confirm with retrieve() before treating an order as paid.
All functions return a normalized response:
{
success: true,
transactionId: 'pi_...',
status: 'succeeded',
amount: 1000,
currency: 'usd',
gatewayResponse: { /* raw gateway response */ },
}BDPayments uses a centralized architecture for making gateway requests, adhering to SOLID and DRY principles:
src/utils/http.js: CentralizedhttpClientwrapping the nativefetchAPI, with timeouts and unified error mapping.src/utils/wrapper.js: Higher-order functionwithErrorHandlingthat maps gateway responses to unifiedPaymentErrorinstances.src/utils/validate.js: Request validation shared by every adapter, so bad input fails before any network call.src/utils/crypto.js: Cryptographic functions (RSA, signatures) decoupled from specific gateway modules.src/utils/cache.js: Reset registry for auth-token and SDK-client caches, cleared byclearConfig().
import { charge, PaymentError, GatewayNotFoundError, ConfigurationError } from 'bdpayments';
try {
await charge({ gateway: 'stripe', amount: 1000, currency: 'usd' });
} catch (error) {
if (error instanceof ConfigurationError) {
console.log('Missing credentials:', error.missingKeys);
} else if (error instanceof GatewayNotFoundError) {
console.log('Try one of:', error.supportedGateways);
} else if (error instanceof PaymentError) {
console.log(`${error.gateway} error [${error.code}]: ${error.message}`);
console.log('HTTP status:', error.status); // null for non-HTTP failures
console.log('Original error:', error.originalError);
}
}| Code | Meaning |
|---|---|
CHARGE_FAILED / EXECUTE_FAILED / REFUND_FAILED / RETRIEVE_FAILED |
The gateway rejected the operation |
INVALID_REQUEST / INVALID_AMOUNT |
Bad input β thrown before any network call |
MISSING_CREDENTIALS |
Required credentials not found |
MISSING_DEPENDENCY |
An optional peer SDK (e.g. stripe) is not installed |
INVALID_CONFIG |
configure() was given an unknown gateway name |
GATEWAY_NOT_FOUND |
Unknown gateway name |
UNSUPPORTED_OPERATION |
The gateway does not implement that operation |
AUTH_FAILED |
Gateway authentication failed (e.g. bKash token grant) |
TIMEOUT / NETWORK_ERROR |
The request never completed |
Full TypeScript definitions are included. Options are narrowed by gateway name, so fields belonging to another gateway are rejected at compile time:
import { charge } from 'bdpayments';
const result = await charge({
gateway: 'stripe',
amount: 1000,
currency: 'usd',
paymentMethod: 'pm_card_visa',
});
await charge({
gateway: 'stripe',
amount: 1000,
currency: 'usd',
invoiceNumber: 'INV-1', // β Error: bKash-only field
});Use extra to pass anything the types don't model β it is forwarded to the gateway untouched.
Nagad's protocol requires RSA PKCS#1 v1.5, which Node.js 18.19.1, 20.11.1 and 21.6.2
disabled for decryption (CVE-2023-46809). On those exact patch releases, Nagad charge()
fails while decrypting the initialization response. Upgrade to a later patch release.
npm install
npm test # unit tests, no network access required
npm run typecheck # type definitions
npm run check # bothReleased under the MIT License. Β© 2024β2026 Sabbir Mahmud