@togtokh.dev/bonum
v0.1.1
Published
Bonum payment gateway api
Maintainers
Readme
BONUM
Node.js / TypeScript client for the Bonum Gateway payment API (API docs).
Install
npm install @togtokh.dev/bonumConfig
| Variable | Testing | Production |
| ----------------------- | ---------------------------------------------- | --------------------- |
| API_BASE_URL | https://testapi.bonum.mn | https://apis.bonum.mn |
| APP_SECRET | provided in the docs testing env | ask Bonum |
| DEFAULT_TERMINAL_ID | provided in the docs testing env | ask Bonum |
| MERCHANT_CHECKSUM_KEY | provided in the docs testing env | ask Bonum |
By default config.host points at production (https://apis.bonum.mn). Set it to the testing
host while developing.
Auth
import BONUM, { config } from "@togtokh.dev/bonum";
config.host = "https://testapi.bonum.mn"; // testing env, defaults to production
BONUM.auth
.CREATE({
appSecret: "<APP_SECRET>",
terminalId: "<DEFAULT_TERMINAL_ID>",
})
.then(async (r) => {
console.log(config.authInfo); // { tokenType, accessToken, expiresIn, refreshToken, refreshExpiresIn, unit }
});auth.CREATE is rate-limited by Bonum, so every other request uses auth.REFRESH automatically
to renew an expired accessToken (see "Token refresh" below) — you normally only call
auth.CREATE once at process start.
Web payment / All in one (invoice)
const providers = await BONUM.invoice.GET_PAYMENT_PROVIDERS();
// [{ provider: "QPAY", enabled: true }, { provider: "E_COMMERCE", enabled: true }, ...]
const invoice = await BONUM.invoice.CREATE({
amount: 1000,
callback: "https://example.com/payments/callback",
transactionId: "a123456789", // unique id, your side, len 1-80
expiresIn: 23000, // seconds
providers: ["QPAY"], // optional, restricts payment options shown on the checkout page
items: [
{
image: "https://example.com/product.png",
title: "Test 1",
remark: "Test Remark 1",
amount: 1000,
count: 1,
},
],
extras: [{ placeholder: "email", type: "EMAIL", required: true }],
});
// redirect the customer's browser to invoice.data.followUpLink to complete the payment.
// once paid (or cancelled/expired), Bonum POSTs a WebHook message to your registered
// webhook URL (see "Webhooks" below) — that is the source of truth for invoice status.invoice.INFO and invoice.SET_PAID exist only for the testing environment, do not use them
in production.
Card Tokenization
const token = await BONUM.cardToken.CREATE({
callback: "https://example.com/card-token-callback",
transactionId: "24za20250512180511006",
payment: { amount: 10 }, // optional, 0.01 MNT charged for verification if omitted
subscription: { planId: 1, cycleValue: "1", cycles: 10 }, // optional
items: [{ title: "Item 6 title", remark: "Item 6 remark", amount: 1, count: 10 }],
});
// redirect the customer to token.data.followUpLink, the generated card token is delivered
// to your webhook as a CARD-TOKEN message once tokenization succeeds.
const purchase = await BONUM.cardToken.PURCHASE("<CARD-TOKEN>", {
amount: 15,
currency: "MNT",
transactionId: "p123456789",
});
// purchase.data.data.status: "SUCCESS" | "FAILED" | "QUEUED" (queued = async, result comes via webhook)
// Caution: do not rely on purchase.data.errorCode, it is for Bonum's internal use only.
await BONUM.cardToken.ROLLBACK("<CARD-TOKEN>", "p123456789");Subscription plans
const plans = await BONUM.subscription.LIST_PLANS();
const subscription = await BONUM.subscription.SUBSCRIBE("<CARD-TOKEN>", {
planId: 1,
cycleValue: "1", // 1-7 WEEKLY, 1-31 MONTHLY, 1-366 YEARLY
cycles: 10, // optional, unlimited if omitted
payNow: false,
custEmail: "[email protected]",
});
await BONUM.subscription.LIST("<CARD-TOKEN>");
await BONUM.subscription.CHANGE_TOKEN_EXISTING(subscription.data.subscriptionId, "<NEW-CARD-TOKEN>");
await BONUM.subscription.UNSUBSCRIBE(subscription.data.subscriptionId, 1); // still runs the due cycle
await BONUM.subscription.DELETE(subscription.data.subscriptionId, 1); // cancels immediatelyAutomatic billing results are delivered as SUBSCRIPTION-PAYMENT / UNSUBSCRIBED webhook
messages, not by polling.
QR code / Deeplink payment
const qr = await BONUM.qr.CREATE({
amount: 10,
transactionId: "3ba1234567890a",
expiresIn: 600, // 10 minutes
});
// qr.data.qrImage (base64), qr.data.qrCode, qr.data.links (bank app deeplinks)
const found = await BONUM.qr.INFO(qr.data.qrCode);
await BONUM.qr.PAY("<CARD-TOKEN>", { qrCode: qr.data.qrCode, transactionId: "3ba1234567890a" });Webhooks
Bonum delivers payment / card-token / subscription results to your registered webhook URL as an
HTTP POST, with an HMAC-SHA256 signature in the x-checksum-v2 header. Verify it using the
raw request body (not a re-serialized copy, since the signature is computed over the exact
bytes sent):
import BONUM from "@togtokh.dev/bonum";
app.post(
"/bonum/webhook",
express.raw({ type: "*/*" }), // keep access to the raw body
(req, res) => {
const rawBody = req.body.toString("utf8");
const valid = BONUM.webhook.verify(
rawBody,
req.headers[BONUM.webhook.CHECKSUM_HEADER] as string,
"<MERCHANT_CHECKSUM_KEY>"
);
if (!valid) return res.status(400).send("invalid checksum");
const message = JSON.parse(rawBody); // WebhookMessageT
// message.type: "PAYMENT" | "CARD-TOKEN" | "SUBSCRIPTION-PAYMENT" | "UNSUBSCRIBED"
// message.status: "SUCCESS" | "FAILED"
res.status(200).send("ok");
}
);You may also set config.merchantChecksumKey once at startup and omit the third argument to
verify/sign.
Token refresh
Every request that gets a 401 automatically retries once, refreshing accessToken via
auth.REFRESH (falling back to a full auth.CREATE if there is no refresh token yet). You do not
need to manage the access token lifecycle yourself.
