paybaji
v1.0.0
Published
Official Node.js SDK for the paybaji payment gateway — accept bKash, Nagad, Rocket, Upay, and Tap payments by verifying user-submitted transaction IDs against the merchant's SMS feed.
Maintainers
Readme
paybaji
Official Node.js SDK for the paybaji payment gateway. Accept bKash, Nagad, Rocket, Upay, and Tap payments from your Bangladeshi customers by verifying their user-submitted transaction IDs against the merchant device's SMS feed. No funds custody — paybaji just confirms the payment happened and notifies your server.
npm install paybajiRequires Node.js 18+ (uses globalThis.fetch). Server-side only — keep the secret key off the browser.
Quick start
import { Paybaji } from 'paybaji';
const paybaji = new Paybaji({
publicKey: process.env.PAYBAJI_PUBLIC_KEY!,
secretKey: process.env.PAYBAJI_SECRET_KEY!,
hmacSecret: process.env.PAYBAJI_HMAC_SECRET!, // for verifyWebhook()
// apiBaseUrl defaults to https://api.paybaji.com
});
// 1. On every deposit-page render, ask what payment options are live for your brand.
// The wallet is randomly load-balanced across your active MFS pool.
const options = await paybaji.getPaymentOptions();
// → [{ provider: 'bkash', channel: 'personal', channel_label: 'Send Money',
// wallet: { id, phone_number, label }, wallet_count }, ...]
// 2. When the user picks a method and submits their deposit amount:
const payment = await paybaji.initiatePayment({
mfsAccountId: options[0].wallet.id,
amount: 500.00,
endUserRef: 'order_42', // echoed back on the webhook
successUrl: 'https://example.com/paid', // optional
failUrl: 'https://example.com/failed' // optional
});
// 3. Redirect the user to paybaji's hosted checkout.
return Response.redirect(payment.checkout_url, 303);The user lands on paybaji's checkout, sees the wallet number + amount, sends from their MFS app, and pastes the TrxID. paybaji matches against the merchant's incoming SMS and fires a signed webhook to your server.
Webhook handler (Express)
import express from 'express';
import { paybaji } from './paybaji';
const app = express();
// IMPORTANT: capture the RAW body before any JSON parsing — the signature
// is computed over the exact bytes paybaji sent.
app.post(
'/paybaji/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const rawBody = req.body.toString('utf8');
const sig = req.header('x-paybaji-signature');
if (!paybaji.verifyWebhook(rawBody, sig)) {
return res.status(401).send('bad signature');
}
const event = JSON.parse(rawBody) as import('paybaji').WebhookPayload;
// event.status === 'matched'
// event.end_user_ref === your `endUserRef` from initiatePayment
// event.txn_id is the TrxID the user submitted
//
// Idempotency: paybaji retries up to 10 times. Dedupe on event.id.
creditOrderIdempotent(event.end_user_ref, event.amount);
res.status(200).send('ok');
},
);Webhook handler (Next.js Route Handler)
// app/api/paybaji-webhook/route.ts
import type { NextRequest } from 'next/server';
import { paybaji } from '@/lib/paybaji';
import type { WebhookPayload } from 'paybaji';
export async function POST(req: NextRequest): Promise<Response> {
const rawBody = await req.text(); // raw bytes for signature
const sig = req.headers.get('x-paybaji-signature');
if (!paybaji.verifyWebhook(rawBody, sig)) {
return new Response('bad signature', { status: 401 });
}
const event = JSON.parse(rawBody) as WebhookPayload;
await creditOrderIdempotent(event.end_user_ref, event.amount);
return new Response('ok', { status: 200 });
}Polling fallback
If your endpoint can't accept inbound webhooks (e.g. behind a corporate firewall), call getPaymentStatus until the status is final (matched, expired, or failed):
const status = await paybaji.getPaymentStatus(paymentId);
// status.status, status.submittedTxnId, status.matchedAtAPI reference
new Paybaji(config)
| Field | Type | Notes |
|---|---|---|
| publicKey | string | Required. pk_test_… or pk_live_… |
| secretKey | string | Required. Never expose to a browser. |
| hmacSecret | string? | Required only if you call verifyWebhook(). |
| apiBaseUrl | string? | Defaults to https://api.paybaji.com. Override for self-hosted. |
| fetch | typeof fetch? | Inject a custom fetch (useful for tests). |
Methods
| Method | Returns |
|---|---|
| getPaymentOptions() | Promise<PaymentOption[]> — currently active (provider, channel) groups with one randomly-picked wallet each |
| initiatePayment(input) | Promise<InitiateResponse> — includes checkout_url to redirect the user to |
| getPaymentStatus(paymentId) | Promise<PaymentStatusResponse> |
| verifyWebhook(rawBody, signatureHeader) | boolean — true iff the HMAC matches AND timestamp is within ±5 min |
All network methods throw PaybajiError on non-2xx responses (the error carries .status and .body).
Authentication recipe (for reference)
Every outgoing request is signed with three headers:
X-Brand-Key: pk_test_xxxxxxxxxxxx
X-Timestamp: 1716906000
X-Signature: hex(HMAC_SHA256(secretKey, `${timestamp}.${rawBody}`))GET requests sign over an empty body (""). The SDK handles all of this for you.
Webhook signature format
X-Paybaji-Signature: t=1716906321,v1=ab12cd34...v1 = hex(HMAC_SHA256(hmacSecret, "${t}.${rawBody}")). Verify against the raw bytes you received — re-stringifying parsed JSON will fail the signature.
TypeScript
Full types are bundled. Import what you need:
import type {
PaybajiConfig,
PaymentOption,
InitiateInput,
InitiateResponse,
PaymentStatusResponse,
WebhookPayload,
PaybajiProvider,
PaybajiChannel,
PaybajiStatus,
} from 'paybaji';License
MIT
