# ACB bank QR API (dynamic VietQR) per order

> Create an ACB VietQR code carrying the order code and amount via POST /acb/qr-payment/generate, get qr_data_url to display; cancel unpaid QR codes. cURL, PHP, Node samples.
> Source: https://monapay.vn/en/docs/api/qr-thanh-toan · Updated: 29/08/2026 · MONA Pay is the payment gateway and bank API of The MONA Group that lets Vietnamese businesses receive and confirm bank transfers in real time via virtual accounts (VA), VietQR, webhooks and Telegram — built so both developers and AI agents can integrate in minutes.

A MONA Pay payment QR is a dynamic VietQR code generated by ACB: the account number (or VA), amount and order code are pre-filled, so the customer scans it with any banking app and pays the exact amount with the exact reference, nothing typed by hand. Call `POST /api/v1/acb/qr-payment/generate` with `orderId`, `amount` (VND, integer, up to 1,000,000,000) and the VA prefix; MONA Pay returns `qr_data_url` for you to render on the checkout page. When the customer pays, ACB reports it and MONA Pay fires the [webhook](/en/docs/webhooks/tich-hop-webhook) exactly like money arriving in a VA. A QR created by mistake can be cancelled through the cancellation endpoint before the customer scans it.

If you would rather not call the API, the dashboard has a Create QR section: pick a VA, enter the amount, download the image to print or send to the customer.

## POST /api/v1/acb/qr-payment/generate

Needs Bearer + `X-Client-Secret`. Requirement: a linked ACB account with a VA (see [Virtual accounts](/en/docs/api/tai-khoan-ao-va)).

| Field | Type | Required | Notes |
|---|---|---|---|
| `ownerNumber` | string | yes | The receiving ACB account number |
| `ownerType` | `PER` / `ORG` | yes | `PER` personal, `ORG` business |
| `merchantId` | string | yes | Merchant code issued by ACB when the QR service was opened. The correct value is shown in the dashboard under Create QR |
| `terminalId` | string | yes | Terminal code issued by ACB, shown next to `merchantId` |
| `orderId` | string | yes | Your order code, unique per payment |
| `virtualAccountPrefix` | string (1-10) | yes | VA prefix |
| `beneficiaryName` | string (1-100) | yes | The beneficiary name shown in the customer's banking app |
| `amount` | integer | yes | VND amount, 0 to 1,000,000,000. `0` = the customer enters the amount |
| `description` | string (≤255) | no | Transfer note pre-filled for the customer |
| `traceNumber` | string | no | Your own tracking code, recommended for lookups |
| `userId` | string | no | Customer id in your system |
| `voucherCode`, `loyaltyCode` | string | no | Discount / loyalty codes if you want them stored |
| `additionalInfo` | array `[{key, value}]` | no | Extra key-value data |

Sample body:

```json
{
  "ownerNumber": "123456789",
  "ownerType": "ORG",
  "merchantId": "MC00012345",
  "terminalId": "TM0001",
  "orderId": "DH10234",
  "virtualAccountPrefix": "MONA",
  "beneficiaryName": "CONG TY ABC",
  "amount": 2500000,
  "description": "Thanh toan DH10234",
  "traceNumber": "DH10234-20260828"
}
```

Response 200: `data` is the QR record; the important fields are `qr_data_url` and `virtual_account_number`.

```json
{
  "success": true,
  "message": "Success",
  "data": {
    "id": "0190c0d1-...",
    "virtual_account_id": "0190b0c3-...",
    "owner_number": "123456789",
    "owner_type": "ORG",
    "merchant_id": "MC00012345",
    "terminal_id": "TM0001",
    "user_id": null,
    "order_id": "DH10234",
    "virtual_account_prefix": "MONA",
    "beneficiary_name": "CONG TY ABC",
    "amount": 2500000,
    "voucher_code": null,
    "loyalty_code": null,
    "description": "Thanh toan DH10234",
    "additional_info": null,
    "virtual_account_number": "MONA0000010234",
    "trace_number": "DH10234-20260828",
    "qr_data_url": "00020101021238...6304ABCD",
    "created_at": "2026-08-28T10:40:00",
    "updated_at": null
  }
}
```

`qr_data_url` is the standard VietQR (EMVCo) data string. Feed it to any QR rendering library (for example `qrcode` for Node, `endroid/qr-code` for PHP) to get an image. Keep `id` in case you need to cancel.

**cURL**

```bash
curl -X POST https://api.monapay.vn/api/v1/acb/qr-payment/generate \
  -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{"ownerNumber":"123456789","ownerType":"ORG","merchantId":"MC00012345","terminalId":"TM0001","orderId":"DH10234","virtualAccountPrefix":"MONA","beneficiaryName":"CONG TY ABC","amount":2500000,"description":"Thanh toan DH10234"}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.monapay.vn/api/v1/acb/qr-payment/generate');
curl_setopt_array($ch, [
    CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . getenv('MONA_TOKEN'), 'X-Client-Secret: ' . getenv('MONA_SECRET')],
    CURLOPT_POSTFIELDS => json_encode([
        'ownerNumber' => '123456789', 'ownerType' => 'ORG', 'merchantId' => getenv('ACB_MERCHANT_ID'), 'terminalId' => getenv('ACB_TERMINAL_ID'),
        'orderId' => $order->code, 'virtualAccountPrefix' => 'MONA', 'beneficiaryName' => 'CONG TY ABC',
        'amount' => (int) $order->total, 'description' => 'Thanh toan ' . $order->code,
    ]),
]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($res['success'])) throw new RuntimeException($res['message'] ?? 'QR generation failed');

$qrString = $res['data']['qr_data_url']; // feed to a QR rendering library
$qrId     = $res['data']['id'];          // keep to cancel if the order is cancelled
```

**Node**

```js
import QRCode from 'qrcode'; // npm i qrcode

const r = await fetch('https://api.monapay.vn/api/v1/acb/qr-payment/generate', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.MONA_TOKEN}`, 'X-Client-Secret': process.env.MONA_SECRET },
  body: JSON.stringify({
    ownerNumber: '123456789', ownerType: 'ORG', merchantId: process.env.ACB_MERCHANT_ID, terminalId: process.env.ACB_TERMINAL_ID,
    orderId: order.code, virtualAccountPrefix: 'MONA', beneficiaryName: 'CONG TY ABC', amount: order.total, description: `Thanh toan ${order.code}`,
  }),
});
const { success, data, message } = await r.json();
if (!success) throw new Error(message);
const pngDataUrl = await QRCode.toDataURL(data.qr_data_url); // <img src=...>
```

## DELETE /api/v1/acb/qr-payment/{qr_code_id}/cancellation

Cancels a QR that was created but not yet paid (order cancelled, amount changed). Needs Bearer + `X-Client-Secret`. The body must repeat the details used at creation:

| Field | Type | Required |
|---|---|---|
| `ownerNumber` | string | yes |
| `ownerType` | `PER` / `ORG` | yes |
| `orderId` | string | yes |
| `amount` | integer | yes |
| `traceNumber` | string | no |

```bash
curl -X DELETE https://api.monapay.vn/api/v1/acb/qr-payment/0190c0d1-.../cancellation \
  -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{"ownerNumber":"123456789","ownerType":"ORG","orderId":"DH10234","amount":2500000}'
```

Response: `{"success": true, "message": "Success", "data": null}`.

## What happens after the customer scans

ACB records the incoming money, notifies MONA Pay, and MONA Pay fires the webhook to your configured URL with the same payload as a VA transaction:

```json
{"amount":2500000,"description":"Thanh toan DH10234","transfer_date":"10:45:12 28/08/2026","transaction_code":"FT26240001234","account_number":"MONA0000010234","bank_name":"ACB","type":"income"}
```

Match the order by `account_number` (the VA attached to the QR) or by the `orderId` inside `description`. Compare `amount` with the order total before marking it paid.

## Common problems

| Symptom | Cause | Fix |
|---|---|---|
| 400 on creation | The account has no VA registered with this prefix, or `merchantId`/`terminalId` is wrong | Check the dashboard under Create QR; create one code on the web first |
| 422 | Missing required field, `amount` above 1,000,000,000, `virtualAccountPrefix` longer than 10 characters | Fix according to `detail[]` |
| Customer can scan but no webhook arrives | The VA is not registered for notifications (steps 3, 4 of VA creation) | See [Virtual accounts](/en/docs/api/tai-khoan-ao-va) |
| QR expired in the banking app | Every dynamic QR has a validity period set by ACB | Cancel the old code and create a new one with the same `orderId` |
