#Server guide
#Configuration
import { BkashServer } from 'bkash-server';
const bkash = new BkashServer({
apiVersion: 'v1.2.0-beta', // required: 'v1.2.0-beta' | 'v2'
appKey, appSecret, username, password,
baseUrl: 'https://tokenized.sandbox.bka.sh',
tokenStore, // optional, see Stores
sessionStore, // optional, see Stores
timeoutMs: 30000,
webhookOptions: { /* see Webhooks */ },
});
| Option | Default | Notes |
|---|---|---|
apiVersion |
none (required) | v1.2.0-beta has the most mileage. v2 is the version bKash now lists as stable, but this SDK's v2 adapter is experimental until a sandbox end-to-end run with v2 credentials passes. |
baseUrl |
none (required) | Host only, for example https://tokenized.sandbox.bka.sh or https://tokenized.pay.bka.sh. The version path is added for you. |
tokenStore |
in-memory | Holds the id_token/refresh_token and the refresh lock. Use Redis or Upstash with more than one process. |
sessionStore |
in-memory | Remembers the expected order (and agreementId) per paymentId between create and execute. |
timeoutMs |
30000 |
bKash documents a 30 s default timeout. |
#Stores
bKash allows Grant Token and Refresh Token at most twice per hour; going over blocks refresh calls for an hour. The token store makes every process share one token.
import Redis from 'ioredis';
import { RedisTokenStore, RedisSessionStore } from 'bkash-server/redis';
const redis = new Redis(process.env.REDIS_URL!);
const bkash = new BkashServer({
/* credentials */
tokenStore: new RedisTokenStore({ redis }),
sessionStore: new RedisSessionStore({ redis }),
});
UpstashRestTokenStore and UpstashRestSessionStore (exported from the root entry) work over HTTP for serverless platforms. The in-memory stores log a warning in production: they are for development only.
#Methods
| Method | Versions | Purpose |
|---|---|---|
createPayment(params) |
v1, v2 | Start a payment. Pass agreementId (v2) to pay with a saved agreement. |
executePayment({ paymentId }) |
v1, v2 | Execute, reconcile on timeout or 2062, and verify amount, currency and invoice. |
queryPayment(paymentId) |
v1, v2 | Current state of a payment. |
searchTransaction({ trxId }) |
v1, v2 | Look up a completed transaction. |
refundPayment(params) |
v1, v2 | Full or partial refund; up to 10 partial refunds within 60 days. |
refundStatus(params) |
v1, v2 | Refund history. v2 returns every refund in refunds. |
createAgreement, executeAgreement, queryAgreement, cancelAgreement |
v2 | Account binding for repeat payments. |
capturePayment, voidPayment |
none | Throw BkashFeatureNotSupportedError: not implemented. |
#Verification
executePayment needs to know what you expected. Either let the sessionStore remember it from createPayment, or pass expectedOrder:
const result = await bkash.executePayment({
paymentId,
expectedOrder: { amount: '1550.00', currency: 'BDT', invoiceNumber: 'INV_1001' },
});
// throws BkashVerificationMismatchError if bKash reports anything else
#Agreements (v2)
// 1. Bind the account once per customer
const a = await bkash.createAgreement({ payerReference: '01XXXXXXXXX', callbackUrl });
// redirect the customer to a.redirectUrl, then, on the callback:
const done = await bkash.executeAgreement({ agreementId: a.agreementId! });
// store done.agreementId against your customer
// 2. Every later payment: the customer only enters the PIN
const p = await bkash.createPayment({ amount: '250.00', invoiceNumber, callbackUrl, agreementId });
// redirect to p.redirectUrl, then on the callback:
await bkash.executePayment({ paymentId: p.paymentId }); // agreementId is remembered by the sessionStore
If Execute Agreement times out, call queryAgreement: retry from Create only when the status is Initiated.
#Refunds
await bkash.refundPayment({ paymentId, trxId, amount: '100.00', reason: 'Item returned', sku: 'SKU-1' });
If a refund call times out, call refundStatus before retrying. A refund is a money movement: do not expose it as a public route. The HTTP handler does not provide one.
#HTTP handler
import { createBkashHandler } from 'bkash-server';
const handle = createBkashHandler(bkash, {
basePath: '/api/bkash',
resolveOrder: async (req, body) => ({ amount, currency: 'BDT', invoiceNumber }),
callbackUrl: 'https://shop.example/api/bkash/callback', // optional fixed value
allowedCallbackOrigins: ['https://shop.example'], // for a client-supplied callbackUrl
allowedRedirectOrigins: ['https://shop.example'], // for returnUrl redirects
onSuccess: async (result) => { await markPaid(result); },
onError: async (error) => undefined,
});
| Route | Behaviour |
|---|---|
POST /create |
Needs resolveOrder (or the unsafe allowClientAmount: true). Ignores amount, invoiceNumber, orderId and payerReference from the browser. A browser callbackUrl is used only if its origin is in allowedCallbackOrigins. |
GET|POST /callback |
Executes, verifies, then calls onSuccess or redirects to an allowlisted returnUrl. cancel and failure return 400 without executing. |
GET /status?paymentId= |
Minimal public fields only; the wallet number is masked. |
POST /webhook |
Verifies the SNS signature first. A message that fails verification gets 401. |
Errors are mapped to minimal responses: timeout → 202 {status:"unknown"}, mismatch → 409, duplicate → 409, rate limit → 429, other bKash errors → 400 with the code, anything else → 500 without internal details.
#Framework adapters
All adapters wrap the same handler.
import { createExpressHandler, createNextHandler, createFastifyHandler, createHonoHandler } from 'bkash-server/frameworks';
// Next.js App Router: app/api/bkash/[...route]/route.ts
export const { GET, POST } = createNextHandler(bkash, { basePath: '/api/bkash', resolveOrder });
Koa, Remix, SvelteKit and Astro adapters are also exported. Express, Fastify, Hono and Koa are tested inside the real frameworks; Next.js, Remix, SvelteKit and Astro are tested with the web-standard Request and Response they pass to handlers. See feature status.
#Runtimes
The root entry uses fetch and Web Crypto and has no Node built-in imports; the webhook verifier lives in bkash-server/webhook because it needs node:crypto. Only Node.js is exercised in CI. Cloudflare Workers, Vercel Edge, Deno and Bun are not tested.