npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

banglapay

v1.0.2

Published

Unified server-side TypeScript SDK for Bangladesh payment gateways: bKash, Nagad and SSLCOMMERZ.

Readme

banglapay

One SDK for every Bangladeshi payment gateway

bKash · Nagad · SSLCOMMERZ — unified behind a single, type-safe, server-side TypeScript API.

npm version types license runtime deps

📖 Documentation


Stop re-wiring three different REST APIs, hand-writing request/response types, and babysitting each gateway's auth quirks on every project. banglapay normalizes all of it: one PaymentClient, one call shape, one status enum.

Swap the gateway field and only the credentials change — every call site stays identical.

const { redirectURL, paymentRef } = await client.initPayment({
  amount: 500, currency: 'BDT', orderId: 'ORD-123',
  callbackURL: 'https://api.myapp.com/payments/callback',
});
const result = await client.executePayment({ paymentRef }); // → normalized StatusResult

Why banglapay

| | | | --- | --- | | Server-side only | Handles store passwords, app secrets, and RSA private keys. Never ships to the browser. | | Framework-independent | Pure TypeScript. Identical on Node 18+, Bun, and Deno. Returns data — never owns your routes. | | Zero runtime deps | Native fetch + Node crypto. Nothing else. | | Type-safe by design | Discriminated-union config — wrong credentials fail at compile time. | | Normalized everything | One PaymentStatus enum, shared result types, and always the raw provider payload. | | Dual ESM + CJS | Ships .mjs, .cjs, and generated .d.ts. |

Checkout flow only — no recurring/agreement or disbursement/payout APIs.


Install

npm install banglapay
# or
pnpm add banglapay
# or
bun add banglapay

Requires Node 18+ (or Bun / Deno) for global fetch and Web Crypto.

[!WARNING] Never bundle this into frontend code. It manipulates secret credentials and signs requests. Keep it in your backend / API layer only.


Quick start

import { PaymentClient } from 'banglapay';

const client = new PaymentClient({
  gateway: 'sslcommerz',        // 'bkash' | 'nagad' | 'sslcommerz'
  mode: 'sandbox',              // 'sandbox' | 'live'
  credentials: {                // shape enforced per-gateway at compile time
    storeId: process.env.SSLC_STORE_ID!,
    storePassword: process.env.SSLC_STORE_PASSWORD!,
  },
});

// 1. Start a checkout — redirect the customer to redirectURL.
const { redirectURL, paymentRef } = await client.initPayment({
  amount: 500,
  currency: 'BDT',
  orderId: 'ORD-123',
  callbackURL: 'https://api.myapp.com/payments/callback',
});

// 2. After the customer returns, confirm/settle the payment.
const result = await client.executePayment({ paymentRef });
if (result.status === 'SUCCESS') {
  // fulfill the order
}

initPayment, executePayment, queryPayment, refund, and verifyWebhook are identical across all three gateways. Only the constructor credentials differ.


Supported gateways

| Gateway | Flow | Auth model | Webhook / callback | | --- | --- | --- | --- | | SSLCOMMERZ | Hosted checkout → validate → refund | Store id + password | Signed IPN (md5 hash) | | bKash | Tokenized Checkout: create → execute | OAuth token (auto-managed) | Unsigned redirect → re-query | | Nagad | Initialize → complete handshake | RSA encrypt + sign | Unsigned redirect → re-query |


Normalized model

Every provider response maps onto shared types, and the untouched provider payload is always available as raw.

enum PaymentStatus {
  PENDING, SUCCESS, FAILED, CANCELLED, REFUNDED, PARTIALLY_REFUNDED
}

interface StatusResult {
  status: PaymentStatus;
  paymentRef: string;
  orderId?: string;
  transactionId?: string;
  amount?: number;
  currency?: 'BDT';
  providerStatus?: string;  // provider's own string, pre-normalization
  raw: unknown;             // untouched provider response
}

Errors

All failures throw a subclass of PaymentError — catch broadly or narrowly.

| Class | Meaning | | --- | --- | | PaymentError | Base class for everything below. | | AuthError | Bad credentials / expired or rejected token. | | ValidationError | Your input failed validation before any network call. | | GatewayError | Provider accepted the request but reported a business error. | | NetworkError | Timeout, DNS, connection reset, or non-JSON transport error. | | SignatureError | Webhook hash / RSA verification failed. |

Each carries gateway, providerCode, httpStatus, and raw where available.


Per-gateway setup

Credentials come from the SSLCOMMERZ merchant panel.

const client = new PaymentClient({
  gateway: 'sslcommerz',
  mode: 'sandbox',
  credentials: {
    storeId: 'yourstore',
    storePassword: 'yourpassword',
  },
});

const { redirectURL, paymentRef } = await client.initPayment({
  amount: 1200, currency: 'BDT', orderId: 'ORD-501',
  callbackURL: 'https://api.myapp.com/payments/sslc/callback',
  customer: { name: 'Karim', email: '[email protected]', phone: '01700000000' },
});
// → redirect customer to redirectURL

// On the callback/IPN: verify the signed payload (see snippet below), then:
const status = await client.queryPayment({ paymentRef });

// Refund (needs an amount; transactionId auto-resolved if omitted):
await client.refund({ paymentRef, amount: 1200, reason: 'customer request' });
  • Hosted checkout → redirect → IPN hash verification → validate/query → refund + refund query are all handled for you.
  • executePayment and queryPayment both resolve status via the transaction query API (SSLCOMMERZ has no separate capture step).
  • Sandbox vs live is only a base-URL switch.

Credentials come from your bKash merchant onboarding.

const client = new PaymentClient({
  gateway: 'bkash',
  mode: 'sandbox',
  credentials: {
    appKey: process.env.BKASH_APP_KEY!,
    appSecret: process.env.BKASH_APP_SECRET!,
    username: process.env.BKASH_USERNAME!,
    password: process.env.BKASH_PASSWORD!,
  },
});

const { redirectURL, paymentRef } = await client.initPayment({
  amount: 500, currency: 'BDT', orderId: 'INV-77',
  callbackURL: 'https://api.myapp.com/payments/bkash/callback',
});
// → redirect customer to redirectURL (bkashURL)

// bKash returns to your callbackURL with ?paymentID=..&status=success — execute to capture:
const result = await client.executePayment({ paymentRef });

// Refund (amount required; trxID auto-resolved if omitted):
await client.refund({ paymentRef, amount: 500, reason: 'refund' });

[!NOTE] Token lifecycle is fully automatic. The SDK caches the bearer token in memory, refreshes it ~60s before its ~1h expiry, retries once on an auth failure, and re-grants if a refresh is rejected. You never touch id_token or refresh_token.

bKash Tokenized Checkout has no signed server-to-server webhook — the browser is redirected to your callbackURL with a status query param. verifyWebhook therefore re-queries bKash for the authoritative status instead of trusting it.

Nagad uses an RSA layer: you encrypt payloads with Nagad's PG public key and sign with your merchant private key; the SDK decrypts/verifies Nagad's responses. Keys may be full PEM or the bare base64 body — both are normalized internally.

const client = new PaymentClient(
  {
    gateway: 'nagad',
    mode: 'sandbox',
    credentials: {
      merchantId: process.env.NAGAD_MERCHANT_ID!,
      merchantPrivateKey: process.env.NAGAD_MERCHANT_PRIVATE_KEY!, // PKCS#8
      pgPublicKey: process.env.NAGAD_PG_PUBLIC_KEY!,
      merchantNumber: process.env.NAGAD_MERCHANT_NUMBER,
    },
  },
  { clientIp: '203.0.113.10' }, // sent as X-KM-IP-V4 (Nagad requires a value)
);

const { redirectURL, paymentRef } = await client.initPayment({
  amount: 750, currency: 'BDT', orderId: 'NGD-9',
  callbackURL: 'https://api.myapp.com/payments/nagad/callback',
});
// The SDK runs Nagad's initialize + complete handshake server-side and hands you
// the customer redirect URL. → redirect customer to redirectURL.

const result = await client.queryPayment({ paymentRef });
  • RSA crypto is implemented against Node crypto and unit-tested in isolation (tests/nagad-crypto.test.ts): PKCS#1 v1.5 padding, PEM normalization, sign, verify, encrypt/decrypt round-trips.
  • Decryption uses RSA_NO_PADDING + manual v1.5 unpadding because modern Node blocks RSA_PKCS1_PADDING on private decryption (CVE-2023-46809) — functionally identical for Nagad, and future-proof.
  • Nagad refund availability depends on your merchant agreement; unsupported accounts surface a GatewayError.

Framework-agnostic callback route

The SDK never owns routes — wire it into whatever router you use. Same pattern everywhere: parse the inbound body into a flat Record<string, string>, call verifyWebhook, then act on the normalized result.

// Pseudocode — works with Express, Fastify, Hono, Next.js route handlers, etc.
async function handleCallback(req, res) {
  // SSLCOMMERZ IPN is form-encoded; bKash/Nagad are query params.
  const payload: Record<string, string> = parseBodyOrQuery(req);

  const result = await client.verifyWebhook({
    payload,
    headers: lowercaseHeaders(req.headers), // only for header-signed gateways
  });

  if (!result.verified) return res.status(400).send('invalid signature');

  switch (result.status) {
    case 'SUCCESS':
      await fulfillOrder(result.orderId, result.transactionId);
      break;
    case 'CANCELLED':
    case 'FAILED':
      await markOrderFailed(result.paymentRef);
      break;
  }
  return res.status(200).send('OK');
}
import express from 'express';
import { PaymentClient } from 'banglapay';

const app = express();
app.use(express.urlencoded({ extended: true })); // SSLCOMMERZ IPN is form-encoded

app.post('/payments/callback', async (req, res) => {
  const result = await client.verifyWebhook({ payload: req.body });
  res.status(result.verified ? 200 : 400).send(result.verified ? 'OK' : 'bad sign');
});

API surface

class PaymentClient implements IPaymentGateway {
  constructor(config: PaymentConfig, options?: PaymentClientOptions);
  initPayment(input: InitPaymentInput): Promise<InitPaymentResult>;
  executePayment(input: { paymentRef: string }): Promise<StatusResult>;
  queryPayment(input: { paymentRef: string }): Promise<StatusResult>;
  refund(input: RefundInput): Promise<RefundResult>;
  verifyWebhook(input: WebhookVerifyInput): Promise<WebhookResult>;
  raw(): IPaymentGateway; // escape hatch to gateway-specific extras
}
  • PaymentConfig is a discriminated union on gateway — TypeScript rejects the wrong or missing credential fields at compile time.
  • options accepts fetchImpl (tests/proxies) and clientIp (Nagad's X-KM-IP-V4).
  • Concrete adapters (SSLCommerzGateway, BkashGateway, NagadGateway) and the status normalizers are also exported for advanced use.

Development

npm install
npm run typecheck   # tsc --noEmit, strict
npm test            # vitest: nagad crypto, webhook verification, status mapping
npm run build       # tsup → dual ESM + CJS + .d.ts in dist/

License

MIT © Pranta Das