Transactions API: look up incoming money by virtual account
Every transaction ACB reports is stored by MONA Pay, even when the webhook to your server failed. Query with GET /api/v1/acb/virtual-account/transactions?virtual_account_number=<VA number>&page=1&limit=100 (Bearer token, limit up to 100). Each record has transaction_code, the bank’s stable reference, which you use as the deduplication key during reconciliation. For a transaction that never reached your server, call the retry endpoint and MONA Pay resends the webhook or Telegram message for exactly that transaction.
GET /api/v1/acb/virtual-account/transactions
Needs Bearer. The system looks the VA up by number, checks it belongs to your account, then returns its transactions.
| Query parameter | Type | Required | Notes |
|---|---|---|---|
virtual_account_number |
string | yes | VA number, e.g. MONA0000010234 |
page |
integer ≥1 | no | Default 1 |
limit |
integer 1-100 | no | Default 10 |
curl "https://api.monapay.vn/api/v1/acb/virtual-account/transactions?virtual_account_number=MONA0000010234&page=1&limit=100" \
-H "Authorization: Bearer $MONA_TOKEN"
Response 200:
{
"success": true,
"message": "Success",
"data": {
"data": [
{
"id": "0190d0e1-...",
"acb_callback_request_id": "0190d0e0-...",
"transaction_status": "SUCCESS",
"transaction_channel": "IBFT",
"transaction_date": "2026-08-28T10:45:12",
"effective_date": "2026-08-28T10:45:12",
"debit_or_credit": "credit",
"amount": 2500000,
"transaction_content": "Thanh toan DH10234",
"transaction_code": "FT26240001234",
"account_number": "123456789",
"va_prefix_cd": "MONA",
"va_nbr": "MONA0000010234",
"attributes": {
"remitter_name": "NGUYEN VAN A",
"remitter_account_number": "9876543210",
"issuer_bank_name": "Vietcombank",
"reference_number": "FT26240001234"
}
}
],
"current_page": 1,
"per_page": 100,
"total": 1,
"last_page": 1,
"start": 1,
"end": 1,
"has_next": false,
"has_prev": false
}
}
Field meanings
| Field | Meaning |
|---|---|
id |
The transaction id inside MONA Pay, used by the retry endpoint |
transaction_status |
Status reported by ACB (SUCCESS means the money is in) |
transaction_channel |
ACB channel (fast transfer, QR…) |
transaction_date |
Transaction time, Vietnam time |
effective_date |
Booking date |
debit_or_credit |
credit = money in, debit = money out. Webhooks currently fire for money in only |
amount |
VND amount, integer |
transaction_content |
The transfer note the customer typed (or the QR pre-filled) |
transaction_code |
Bank reference, stable across resends, deduplication key |
account_number |
The real ACB account that received the money |
va_prefix_cd, va_nbr |
Prefix and number of the VA matched to the transaction, null when money went straight to the main account |
attributes |
Extra data from ACB: remitter name and account, remitting bank, reference number, custom1..custom10 when present |
The webhook sends your server a condensed version of this same record (see Payload format).
Reconciling through the API
There is no since_id parameter yet; reconcile as follows (cron every 15-30 minutes or end of day):
- For each VA in use, call page 1 with
limit=100, keep reading whilehas_nextistrue. - For each record, look up
transaction_codein your transactions table. Skip if present; otherwise insert it and process the order as if a webhook had arrived. - Stop when you hit a
transaction_codeyou already have whosetransaction_dateis older than the previous reconciliation mark (the list is newest first).
Details and samples in Reconciliation.
PHP: read every page
<?php
function monaGet(string $url): array {
$ctx = stream_context_create(['http' => ['header' => 'Authorization: Bearer ' . getenv('MONA_TOKEN') . "\r\n", 'ignore_errors' => true]]);
return json_decode(file_get_contents($url, false, $ctx), true) ?? [];
}
$va = 'MONA0000010234'; $page = 1;
do {
$res = monaGet("https://api.monapay.vn/api/v1/acb/virtual-account/transactions?virtual_account_number=$va&page=$page&limit=100");
if (empty($res['success'])) throw new RuntimeException($res['message'] ?? 'Query failed');
foreach ($res['data']['data'] as $tx) {
if ($tx['debit_or_credit'] !== 'credit') continue;
// INSERT ... ON DUPLICATE KEY (UNIQUE transaction_code) → only new records are written
recordTransaction($tx['transaction_code'], $tx['amount'], $tx['transaction_content'], $tx['transaction_date']);
}
$page++;
} while (!empty($res['data']['has_next']));
Node
async function* monaTransactions(va) {
for (let page = 1; ; page++) {
const r = await fetch(`https://api.monapay.vn/api/v1/acb/virtual-account/transactions?virtual_account_number=${va}&page=${page}&limit=100`, {
headers: { Authorization: `Bearer ${process.env.MONA_TOKEN}` },
});
const j = await r.json();
if (!j.success) throw new Error(j.message);
yield* j.data.data;
if (!j.data.has_next) break;
}
}
for await (const tx of monaTransactions('MONA0000010234')) {
if (tx.debit_or_credit !== 'credit') continue;
await db.upsertByTransactionCode(tx.transaction_code, tx); // UNIQUE(transaction_code)
}
POST /api/v1/acb/virtual-account/transactions/{transaction_id}/retry
Resends the notification for one specific transaction (your server was down at the time, or the webhook configuration was wrong and has been fixed). Needs Bearer + X-Client-Secret. Scheduled automatic retries are in progress; today it is this call or the Resend button in the dashboard.
| Field | Type | Required | Notes |
|---|---|---|---|
target_type |
WEBHOOK / TELEGRAM |
yes | Channel to resend on |
target_id |
uuid | no | A specific webhook or Telegram configuration id. Empty = resend to every active configuration matching the transaction |
curl -X POST https://api.monapay.vn/api/v1/acb/virtual-account/transactions/0190d0e1-.../retry \
-H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \
-H 'Content-Type: application/json' -d '{"target_type":"WEBHOOK"}'
The resent payload is identical to the first one, same transaction_code, so your server must deduplicate (see Retries and error handling).
Common errors
| HTTP | Cause | Fix |
|---|---|---|
| 400 | VA number does not exist or does not belong to the account | Get the right number from GET /api/v1/acb/{bank_account_id}/virtual-account/retrieve |
| 401 | Token expired | Log in again |
| 422 | limit > 100 or virtual_account_number missing |
Fix the parameters |
| 404 (retry) | transaction_id does not exist |
Take id from the transaction list |