Skip to content

About

SDK Node.js/TypeScript cho MONA Pay — cổng thanh toán & API ngân hàng Việt Nam (VA, VietQR, webhook HMAC)

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

@monapay/node

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.

Install

npm install @monapay/node

Quick start

import { 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);

Usage

Client

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_in and reused.
  • A request that fails with HTTP 401 is retried once with a fresh token.
  • Methods return the data field of the API response.
  • Errors are thrown as MonaPayError with status and body.
  • checkouts.create and checkouts.cancel send an Idempotency-Key header. Pass { idempotencyKey } as the second argument to set it yourself; otherwise a random UUID is used.

Sub-clients

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.

Test with the sandbox

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.

Dynamic VietQR

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.

Connect a bank account with OTP

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));

Transactions

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',
});

Webhooks

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.

Email notifications

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.

Configuration

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

Development

npm test   # builds dist/ and runs node --test

License

MIT

MONA Pay is part of MONA Cloud by The MONA Group.

About

SDK Node.js/TypeScript cho MONA Pay — cổng thanh toán & API ngân hàng Việt Nam (VA, VietQR, webhook HMAC)

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages