Tài khoản ngân hàng ảo (virtual account) ACB: tạo VA qua API

Tài khoản ảo (VA) là số tài khoản phụ do ACB cấp dưới tài khoản thật của anh chị, theo đầu số (prefix) đã đăng ký. Tiền chuyển vào VA vẫn nằm trong tài khoản ACB thật, nhưng mỗi VA gắn được với 1 đơn hàng hoặc 1 khách nên MONA Pay khớp tiền tự động, không cần đọc nội dung chuyển khoản. Tạo VA qua API gồm 4 bước: gửi yêu cầu đăng ký, nhập OTP ACB gửi về số điện thoại, đăng ký nhận thông báo giao dịch, nhập OTP lần 2. Điều kiện: tài khoản ACB đứng tên anh chị và số điện thoại đang đăng ký với ACB.

Anh chị không muốn gọi API thì dashboard my.monapay.vn có sẵn wizard 4 bước y hệt, tại mục Ngân hàng & VA.

Luồng 4 bước

Bước Endpoint Ai làm gì
1 POST /api/v1/acb/virtual-account/registration Gửi số tài khoản, số điện thoại, đầu số VA. ACB nhận yêu cầu, gửi OTP về điện thoại
2 POST /api/v1/acb/{acb_request_id}/virtual-account/verification Nhập OTP. ACB tạo VA, trả số VA
3 POST /api/v1/acb/{virtual_account_id}/notification/registration Đăng ký nhận thông báo giao dịch tức thì. ACB gửi OTP lần 2
4 POST /api/v1/acb/{acb_request_id}/notification/verification Nhập OTP lần 2. Từ đây tiền vào là có thông báo về MONA Pay

Bỏ bước 3 và 4 thì VA có tồn tại nhưng ACB không báo giao dịch về, webhook và Telegram sẽ im lặng. Đây là lỗi tụi em gặp nhiều nhất khi khách tự làm.

Mọi request dưới đây cần Authorization: BearerX-Client-Secret (xem Xác thực).

Bước 1: POST /api/v1/acb/virtual-account/registration

Trường Kiểu Bắt buộc Ghi chú
bank_account_id uuid không Dùng khi tài khoản ACB đã nối trước đó (lấy từ GET /api/v1/client/bank-accounts). Có trường này thì bỏ account_number, phone_number
customer_type string không Loại khách hàng theo mã ACB, ví dụ PERS cho cá nhân
account_number integer không Số tài khoản thanh toán ACB (số thật)
phone_number string không Số điện thoại đăng ký với ACB, nhận OTP
virtual_account_info.virtual_account_prefix_code string Đầu số VA đã đăng ký với ACB
virtual_account_info.virtual_account_content string không Nội dung định danh gắn với VA (mã đơn, mã khách)
virtual_account_info.virtual_account_explain string không Diễn giải khi đăng ký
virtual_account_info.beneficiary_name_rule integer không Cách hiển thị tên đơn vị hưởng theo quy ước ACB
user_agreement boolean không Khách đồng ý điều khoản dịch vụ, nên gửi true

Body mẫu (lần đầu, chưa có bank_account_id):

{
  "customer_type": "PERS",
  "account_number": 123456789,
  "phone_number": "0901234567",
  "virtual_account_info": {
    "virtual_account_prefix_code": "MONA",
    "virtual_account_content": "DH10234",
    "virtual_account_explain": "Don hang 10234"
  },
  "user_agreement": true
}

Response 200: data là tài khoản ngân hàng vừa ghi nhận, kèm acb_request chứa id cần cho bước 2.

{
  "success": true,
  "message": "Success",
  "data": {
    "id": "0190b0c1-...",
    "account_number": "123456789",
    "acb_request": { "id": "0190b0c2-...", "status": "PENDING", "created_at": "2026-08-28T10:31:00" }
  }
}

cURL

curl -X POST https://api.monapay.vn/api/v1/acb/virtual-account/registration \
  -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{"customer_type":"PERS","account_number":123456789,"phone_number":"0901234567","virtual_account_info":{"virtual_account_prefix_code":"MONA","virtual_account_content":"DH10234"},"user_agreement":true}'

Bước 2: POST /api/v1/acb/{acb_request_id}/virtual-account/verification

Trường Kiểu Bắt buộc
code string có, là OTP ACB gửi về điện thoại
curl -X POST https://api.monapay.vn/api/v1/acb/0190b0c2-.../virtual-account/verification \
  -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \
  -H 'Content-Type: application/json' -d '{"code":"123456"}'

Response 200: data là VA vừa tạo.

{
  "success": true,
  "message": "Success",
  "data": {
    "id": "0190b0c3-...",
    "bank_account_id": "0190b0c1-...",
    "prefix_code": "MONA",
    "content": "DH10234",
    "explain": "Don hang 10234",
    "beneficiary_name_rule": null,
    "virtual_account_number": "MONA0000010234",
    "created_at": "2026-08-28T10:32:00",
    "updated_at": null
  }
}

virtual_account_number là số anh chị đưa cho khách chuyển tiền (hoặc đưa vào QR). id dùng cho bước 3.

Bước 3: POST /api/v1/acb/{virtual_account_id}/notification/registration

Trường Kiểu Bắt buộc Ghi chú
receive_noti_realtime boolean true = báo ngay từng giao dịch (cần cho webhook). false = ACB gộp báo cuối ngày
username string (≤50) không Thông tin đăng nhập theo yêu cầu ACB, để trống nếu không được ACB yêu cầu
curl -X POST https://api.monapay.vn/api/v1/acb/0190b0c3-.../notification/registration \
  -H "Authorization: Bearer $MONA_TOKEN" -H "X-Client-Secret: $MONA_SECRET" \
  -H 'Content-Type: application/json' -d '{"receive_noti_realtime":true}'

Response 200/201: data chứa acb_request.id cho bước 4.

Bước 4: POST /api/v1/acb/{acb_request_id}/notification/verification

Body {"code": "<OTP lần 2>"}, giống bước 2. Thành công là xong: từ giao dịch kế tiếp, ACB báo về MONA Pay, MONA Pay bắn webhook hoặc Telegram theo cấu hình của anh chị.

Ví dụ PHP: chạy trọn bước 1 và 2

<?php
$base = 'https://api.monapay.vn';
$headers = [
    'Content-Type: application/json',
    'Authorization: Bearer ' . getenv('MONA_TOKEN'),
    'X-Client-Secret: ' . getenv('MONA_SECRET'),
];
function call(string $url, array $headers, array $body): array {
    $ch = curl_init($url);
    curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_POSTFIELDS => json_encode($body), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30]);
    $res = json_decode(curl_exec($ch), true) ?? [];
    curl_close($ch);
    if (empty($res['success'])) throw new RuntimeException($res['message'] ?? 'Lỗi không rõ');
    return $res['data'];
}

// Bước 1: gửi yêu cầu, ACB gửi OTP về điện thoại
$reg = call("$base/api/v1/acb/virtual-account/registration", $headers, [
    'customer_type' => 'PERS', 'account_number' => 123456789, 'phone_number' => '0901234567',
    'virtual_account_info' => ['virtual_account_prefix_code' => 'MONA', 'virtual_account_content' => 'DH10234'],
    'user_agreement' => true,
]);
$requestId = $reg['acb_request']['id'];

// Bước 2: nhập OTP (đọc từ form của anh chị)
$va = call("$base/api/v1/acb/$requestId/virtual-account/verification", $headers, ['code' => $_POST['otp']]);
echo 'Số VA: ' . $va['virtual_account_number'];

Ví dụ Node: bước 3 và 4

const base = 'https://api.monapay.vn';
const headers = {
  'Content-Type': 'application/json',
  Authorization: `Bearer ${process.env.MONA_TOKEN}`,
  'X-Client-Secret': process.env.MONA_SECRET,
};
async function call(path, body) {
  const r = await fetch(base + path, { method: 'POST', headers, body: JSON.stringify(body) });
  const j = await r.json();
  if (!j.success) throw new Error(j.message);
  return j.data;
}

// Bước 3: đăng ký nhận thông báo tức thì, ACB gửi OTP lần 2
const noti = await call(`/api/v1/acb/${vaId}/notification/registration`, { receive_noti_realtime: true });
// Bước 4: xác thực OTP lần 2
await call(`/api/v1/acb/${noti.acb_request.id}/notification/verification`, { code: otpLan2 });

Truy vấn và quản lý

Endpoint Việc
GET /api/v1/client/bank-accounts?page=1&limit=10 Danh sách tài khoản ACB đã nối (lấy bank_account_id)
GET /api/v1/acb/{bank_account_id}/virtual-account/retrieve?virtual_account_number=&page=1&limit=10 Danh sách VA của một tài khoản, lọc theo số VA, limit tối đa 100
GET /api/v1/acb/{virtual_account_id}/notification/details Xem đăng ký thông báo của VA
POST /api/v1/acb/{acb_notification_id}/notification/modification Đổi kiểu nhận thông báo (body như bước 3), cần OTP xác thực lại qua bước 4
POST /api/v1/acb/{acb_notification_id}/notification/delete Hủy nhận thông báo
POST /api/v1/acb/{virtual_account_id}/virtual-account/delete Hủy VA, không có body. ACB có thể yêu cầu OTP xác thực qua endpoint verification

Lỗi thường gặp

Tình huống Nguyên nhân Cách xử lý
400 sau bước 1 Số tài khoản không phải ACB, sai số điện thoại đăng ký với ACB, hoặc đầu số VA chưa được ACB cấp Kiểm tra lại với ACB; đầu số VA cần đăng ký trước với ACB
400 code sai ở bước 2/4 OTP nhập sai hoặc hết hạn Làm lại bước trước để ACB gửi OTP mới
VA tạo xong nhưng không thấy giao dịch Chưa làm bước 3 và 4 Gọi notification/registration rồi xác thực OTP lần 2
401 Token hết hạn hoặc thiếu X-Client-Secret Login lại, kiểm tra header
422 Thiếu virtual_account_info.virtual_account_prefix_code Trường này bắt buộc