Node.js/TypeScript SDK for the MONA Pay API: create checkout links and VietQR codes, manage virtual accounts, webhooks and email notifications, and verify signed webhooks.
The SDK has no runtime dependencies and uses the built-in fetch of Node.js 18 or later. It ships ESM, CommonJS and TypeScript type definitions.
npm install @monapay/nodeimport { MonaPay } from '@monapay/node';
const mona = MonaPay.fromEnv();
const checkout = await mona.checkouts.create({
amount: 250000,
order_code: 'DH10234',
return_url: 'https://shop.example/payment/return',
});
console.log(checkout.checkout_url);import { MonaPay } from '@monapay/node';
// From environment variables (see Configuration)
const mona = MonaPay.fromEnv();
// Or with explicit credentials
const client = new MonaPay({
clientId: process.env.MONAPAY_CLIENT_ID,
clientSecret: process.env.MONAPAY_CLIENT_SECRET,
});
console.log(await mona.me());- The first call fetches an OAuth token. The token is cached until 60 seconds before
expires_inand reused. - A request that fails with HTTP 401 is retried once with a fresh token.
- Methods return the
datafield of the API response. - Errors are thrown as
MonaPayErrorwithstatusandbody. checkouts.createandcheckouts.cancelsend anIdempotency-Keyheader. Pass{ idempotencyKey }as the second argument to set it yourself; otherwise a random UUID is used.
| Sub-client | Methods |
|---|---|
keys |
generate, list, destroy |
bankAccounts |
list |
va |
register, verify, registerNotification, verifyNotification, notificationDetail, list |
paymentProfile |
get, set |
checkouts |
create, get, list, cancel |
qr |
generate, cancel |
transactions |
list, iterate, retry |
sandbox |
transaction |
webhooks |
list, create, update, remove, test |
webhookLogs |
list, stats |
emailConfigs |
list, get, create, update, remove, verify, resendVerification, test |
emailLogs |
list, stats |
emailSuppressions |
list, remove |
The client also exposes me() and the shortcuts registerVirtualAccount, verifyVirtualAccount, registerNotification, verifyNotification and notificationDetail.
No bank connection is needed.
const checkout = await mona.checkouts.create({
amount: 10000,
order_code: 'DH10234',
return_url: 'https://shop.example/payment/return',
sandbox: true,
});
await mona.sandbox.transaction({
virtual_account_number: checkout.bank.account_number,
amount: checkout.amount,
description: checkout.order_code,
});
const paid = await mona.checkouts.get(checkout.id);
console.log(paid.status); // "paid"The CHECKOUT_PAID webhook carries the checkout in checkout_id, not id.
const qr = await mona.qr.generate({
ownerNumber: '123456789', ownerType: 'ORG',
merchantId: 'MC00012345', terminalId: 'TM0001', orderId: 'DH10234',
virtualAccountPrefix: 'MONA', beneficiaryName: 'CONG TY ABC',
amount: 2500000, description: 'Thanh toan DH10234',
});
console.log(qr.qr_data_url);qr.cancel(id, body) accepts an optional body for accounts whose bank configuration requires ownerNumber, ownerType, orderId and amount again.
The bank sends each OTP to the account owner's registered phone number. Your app must ask the user for the code at the verify steps; do not generate or store OTPs.
const registration = await mona.registerVirtualAccount({
customer_type: 'PERS',
account_number: 123456789,
phone_number: '0901234567',
virtual_account_info: {
virtual_account_prefix_code: 'LOC',
virtual_account_content: 'DH10234',
virtual_account_explain: 'Don hang 10234',
},
user_agreement: true,
});
const va = await mona.verifyVirtualAccount(registration.acb_request.id, firstOtpFromUser);
const notification = await mona.registerNotification(va.id);
await mona.verifyNotification(notification.acb_request.id, secondOtpFromUser);
console.log(await mona.notificationDetail(va.id));for await (const tx of mona.transactions.iterate({
virtualAccountNumber: 'MONA0000010234', limit: 100,
})) {
console.log(tx.transaction_code, tx.amount);
}
await mona.transactions.retry('transaction-id', {
targetType: 'WEBHOOK', // or 'TELEGRAM'
targetId: 'webhook-config-id',
});Register an HMAC webhook:
await mona.webhooks.create({
name: 'Web ban hang',
webhook_url: 'https://shop.example/webhooks/monapay',
auth_type: 'HMAC_SHA256',
secret_key: process.env.MONA_WEBHOOK_SECRET,
payload_format: 'application/json',
});Verify incoming requests against the raw body. Do not re-serialize a parsed object: the signature covers the exact request bytes. verifyWebhook reads the X-Mona-Timestamp and X-Mona-Signature headers and rejects timestamps older than toleranceSec (default 300).
import { verifyWebhook } from '@monapay/node';
const rawBody = new Uint8Array(await request.arrayBuffer());
const result = verifyWebhook({
rawBody,
headers: request.headers,
secret: process.env.MONA_WEBHOOK_SECRET,
});
if (!result.ok) return new Response(result.reason, { status: 401 });
console.log(result.payload.transaction_code);With Express, put a raw-body parser on the route before the SDK middleware:
import express from 'express';
import { expressWebhook } from '@monapay/node';
const app = express();
app.post('/webhooks/monapay', express.raw({ type: 'application/json' }),
expressWebhook(process.env.MONA_WEBHOOK_SECRET, async (payload, req, res) => {
await saveOnce(payload.transaction_code, payload);
res.status(200).json({ ok: true });
}));A Next.js App Router handler is in examples/nextjs-route.js. Store transaction_code under a unique constraint so redelivered webhooks are not processed twice.
MONA Pay sends a 6-digit code to each new address. Ask the user for the code from their inbox, then verify it.
const config = await mona.emailConfigs.create({ name: 'Accounting', recipients: ['accounting@shop.example'] });
const email = config.pending_verification[0];
const code = await askUserForCode(email);
await mona.emailConfigs.verify(config.id, { email, code });
await mona.emailConfigs.test(config.id);
console.log(await mona.emailLogs.list({ configId: config.id, status: 'sent' }));Bounced or complained addresses appear in emailSuppressions.list(). Call emailSuppressions.remove(email) only after fixing the cause.
MonaPay.fromEnv() reads:
| Variable | Purpose |
|---|---|
MONAPAY_CLIENT_ID, MONAPAY_CLIENT_SECRET |
API key credentials (preferred) |
MONAPAY_USERNAME, MONAPAY_PASSWORD |
Legacy password login; does not work for accounts with 2FA enabled |
MONAPAY_BASE_URL |
API base URL, defaults to https://api.monapay.vn |
The examples above also use MONA_WEBHOOK_SECRET for the webhook signing secret.
Documentation: https://monapay.vn/docs
npm test # builds dist/ and runs node --testMIT
MONA Pay is part of MONA Cloud by The MONA Group.