puranpay
v1.0.0
Published
Official Node SDK for the PuranPay payment gateway — create checkouts, verify webhooks, and fulfill bKash, Nagad, Rocket, and Upay payments.
Maintainers
Readme
PuranPay
Install this on your backend. Create a checkout, send the customer to pay (bKash, Nagad, Rocket, Upay), then fulfill after a signed webhook.
Node 18+. Zero runtime dependencies.
1. Install
In your server folder:
npm install puranpay2. Env
Copy keys from the PuranPay dashboard. Keep them on the server only.
# Dashboard → API keys (sk_test_ while integrating, sk_live_ in production)
PURANPAY_SECRET_KEY=sk_live_...
# Dashboard → Settings → Webhook
PURANPAY_WEBHOOK_SECRET=...
# Must match a shop on your plan (host only, no https://)
PURANPAY_SHOP_DOMAIN=yourshop.com
# Optional. Default is https://api.puranpay.com
PURANPAY_API_URL=https://api.puranpay.comLocal webhook URLs must use 127.0.0.1, not localhost.
3. Client
One file on the server. fromEnv() reads the variables above.
import PuranPay from "puranpay";
const pay = PuranPay.fromEnv();CommonJS:
const PuranPay = require("puranpay").default;
const pay = PuranPay.fromEnv();4. Create a checkout
Redirect the customer to checkoutUrl. We host the pay page.
import express from "express";
import PuranPay, { PuranPayError } from "puranpay";
const pay = PuranPay.fromEnv();
const app = express();
app.post("/checkout", express.json(), async (req, res) => {
try {
const { checkoutUrl, payment } = await pay.payments.create(
{
amount: req.body.amount,
orderId: req.body.orderId,
note: req.body.note,
successUrl: "https://yourshop.com/thanks",
failUrl: "https://yourshop.com/checkout",
},
{ idempotencyKey: req.body.orderId },
);
res.json({ checkoutUrl, paymentId: payment.id });
} catch (error) {
if (error instanceof PuranPayError) {
res.status(error.status >= 400 ? error.status : 502).json({ error: error.message });
return;
}
throw error;
}
});On the shop, send the customer to checkoutUrl. If payment.status is VERIFIED, the order is already paid — do not redirect.
Same idempotencyKey while the checkout is still PENDING returns that checkout (double-click safe). After expire or cancel, the same key opens a new payment, so Pay now on ord-123 works again. Already VERIFIED returns the paid payment.
successUrl / failUrl must be HTTPS on a shop domain on your plan. After pay, we append payment, status, and orderId to the query string.
Own pay page instead of hosted
Create on the server, keep them on your site, drive wallets yourself:
const { payment } = await pay.payments.create(
{ amount: 349, orderId: "ORD-1001" },
{ idempotencyKey: "ORD-1001" },
);
const session = await pay.checkout.retrieve(payment.id);
await pay.checkout.selectMethod(payment.id, "bkash");
await pay.checkout.submitTrx(payment.id, customerTrxId);Public checkout calls need no API key. Still fulfill only from the webhook.
5. Fulfill on the webhook
Do not fulfill because the browser redirected. Fulfill after the HMAC matches.
Mount this route before express.json(), with express.raw:
app.post("/webhooks/puranpay", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = pay.webhooks.constructEvent(req.body, req.header("x-puranpay-signature"));
} catch {
res.status(401).end();
return;
}
if (event.type === "payment.verified" && event.livemode) {
fulfill(event.payment.orderId, event.payment);
}
res.status(200).end();
});Dashboard → Settings → Webhook URL:
https://yourshop.com/webhooks/puranpayUse event.payment.orderId as your idempotency key so a retry does not ship twice. Return 200 quickly.
{
"id": "evt_pay_xxx_verified",
"event": "payment.verified",
"livemode": true,
"payment": {
"id": "pay_xxx",
"amount": 349,
"orderId": "ORD-1001",
"status": "VERIFIED",
"trxId": "ABC1234XYZ"
}
}Other server calls
const payment = await pay.payments.retrieve("pay_xxx");
await pay.payments.cancel("pay_xxx"); // PENDING only
const page = await pay.payments.list({ status: "PENDING", limit: 50 });
// page.payments, page.hasMore, page.nextCursor| Status | Meaning |
|---|---|
| PENDING | Waiting for Send Money + TrxID |
| VERIFIED | Official SMS matched — safe to fulfill |
| EXPIRED | Time ran out |
| CANCELLED | Cancelled while pending |
Create fields
| Field | Notes |
|---|---|
| amount | BDT, > 0, max 1,000,000 |
| orderId | Your id. Comes back on the webhook |
| note | Shown on checkout |
| customer | { name, email, phone } |
| idempotencyKey | Same key → same open checkout. After expire/cancel, Pay now with that key starts a new one. Already paid → returns that payment. |
| successUrl / failUrl | HTTPS on an allowed shop domain |
Sandbox
Dashboard → Sandbox → sk_test_. Same create call. No plan, no phone, no SMS.
const test = new PuranPay({ secretKey: process.env.PURANPAY_SECRET_KEY }); // sk_test_...
const { payment } = await test.payments.create({ amount: 10, orderId: "SANDBOX-1" });
await test.sandbox.complete(payment.id); // webhook fires with livemode: falseDo not fulfill test payments in production. Check event.livemode.
Errors
import { PuranPayError } from "puranpay";
try {
await pay.payments.create({ amount: 349, orderId: "ORD-1001" });
} catch (error) {
if (error instanceof PuranPayError) {
console.log(error.status, error.message);
}
}| Status | Meaning |
|---|---|
| 400 | Bad amount or field |
| 401 | Missing/revoked key |
| 402 | No active plan |
| 403 | Shop domain not on the plan |
| 404 | Unknown pay_ id |
| 409 | Already settled, or TrxID reused |
License
MIT
