Yêu cầu thanh toán
Mỗi đơn cần thu tiền là một yêu cầu thanh toán: số tiền, tài khoản nhận và một mã riêng ghi trong nội dung chuyển khoản. Tiền về đúng mã, BankOpenAPI cập nhật trạng thái và gửi webhook.
Trạng thái
| status | Ý nghĩa | Webhook |
|---|---|---|
pending | Chờ thanh toán: chưa nhận tiền, còn hạn. | Không có |
paid | Nhận đủ tiền. | payment_request.paid |
overpaid | Nhận nhiều hơn số tiền cần thu. Phần thừa bạn tự xử lý với người trả. | payment_request.paid |
underpaid | Nhận ít hơn. Người trả chuyển thêm phần còn thiếu với cùng mã thì chuyển sang paid hoặc overpaid. | payment_request.underpaid |
expired | Quá expiresAt mà chưa nhận tiền. Tiền về sau đó vẫn được khớp như bình thường. | Không có |
canceled | Đã hủy. Tiền về sau đó không khớp với yêu cầu này nữa. | Không có |
Mỗi lần tiền về khớp mã, BankOpenAPI cộng vào amountReceived rồi tính lại trạng thái. Yêu cầu đã paid, overpaid hoặc canceled không nhận thêm tiền; khoản chuyển sau đó chỉ đến dưới dạng sự kiện transaction.received.
QR động
Với ngân hàng và loại tài khoản có QR động, mỗi yêu cầu có một mã QR riêng của ngân hàng (qr.type là dynamic). Người trả quét mã này thì tiền về được khớp theo chính mã QR, kể cả khi họ sửa hay xóa nội dung chuyển khoản. QR động dùng một lần và hết hạn cùng yêu cầu. Ngân hàng có QR động: MB (mọi loại tài khoản), OCB (cá nhân, doanh nghiệp), Vietcombank (hộ kinh doanh, doanh nghiệp), BIDV, VietinBank và VPBank (doanh nghiệp).
- Ngân hàng không có QR động thì
qr.typelàstatic(VietQR tĩnh), khớp theo mãTT…như trước. Code của bạn không cần phân biệt: luôn vẽqr.payload. - Người trả vẫn chuyển khoản tay được tới
transfer.accountNumbervới nội dungtransfer.content. - Yêu cầu đã nhận một phần (
underpaid) thìqrchuyển sang VietQR tĩnh cho số tiền còn thiếu. - Số tiền ghi nhận luôn là số tiền thật về tài khoản. Vài app ngân hàng vẫn cho sửa số tiền của QR động, nên trạng thái
underpaid/overpaidvẫn có thể xảy ra.
Đối tượng payment_request
Các trường
idstringID của yêu cầu, tiền tốpr_.objectstringLuôn là"payment_request".livemodebooleanamountintegerSố tiền cần thu, đơn vị VND.amountReceivedintegerTổng số tiền đã nhận và khớp với yêu cầu này.currencystringLuôn là"VND".statusenumcodestringMã người trả ghi trong nội dung chuyển khoản:TT+ 6 ký tự, không có 0, O, 1, I, L. Xem VietQR và nội dung chuyển khoản.orderIdstring | nullMã đơn hàng bên hệ thống của bạn, như lúc tạo.descriptionstring | nullMô tả ngắn như lúc tạo, hiện cho người trả ở trang thanh toán (hostedUrl).bankAccountobjectTài khoản ngân hàng nhận tiền của yêu cầu.Các trường con
idstringID tài khoản ngân hàng, tiền tốba_.bank.binstringMã BIN 6 chữ số của ngân hàng (chuẩn NAPAS).bank.namestring | nullTên ngắn của ngân hàng, ví dụMB Bank.accountNumberstringSố tài khoản đã liên kết.labelstringNhãn để hiển thị, che bớt số tài khoản:MB Bank •••• 6789.
transferobjectThông tin chuyển khoản cho người trả, dùng để dựng mã VietQR.Các trường con
bankBinstringMã BIN ngân hàng nhận.accountNumberstringTài khoản nhận báo có. Có thể là số virtual account (tài khoản định danh) gắn với tài khoản của bạn, khácbankAccount.accountNumber. Luôn dùng số này khi dựng QR.accountNamestringTên chủ tài khoản, in hoa không dấu.amountintegerSố tiền còn phải chuyển:amount − amountReceived, không nhỏ hơn 0.contentstringNội dung chuyển khoản, chính làcode.
qrobject | nullMã QR cho người trả quét, đã dựng sẵn.nullkhi yêu cầu không còn nhận tiền (paid,overpaid,expired,canceled). Xem QR động.Các trường con
typeenumdynamic: QR động của ngân hàng, dùng một lần, khớp đơn theo chính mã QR nên đúng cả khi người trả sửa nội dung.static: VietQR tĩnh tớitransfer.accountNumber, khớp theo mãTT…trong nội dung.payloadstringChuỗi QR chuẩn EMVCo/VietQR. Vẽ thành ảnh bằng thư viện QR bất kỳ, không cần tự dựng.expiresAtstring | nullHạn của QR động, bằngexpiresAtcủa yêu cầu.nullvới VietQR tĩnh.
hostedUrlstringTrang thanh toán do BankOpenAPI dựng sẵn cho yêu cầu này: mã VietQR, thông tin chuyển khoản, tự báo khi nhận tiền. Gửi link cho người trả nếu bạn không tự hiện QR.paymentLinkIdstring | nullLink thanh toán đã tạo ra yêu cầu này (khi người trả mở link thanh toán của bạn).nullvới yêu cầu tạo bằng API hoặc trong app.expiresAtstringHạn thanh toán. Quá hạn mà chưa nhận tiền thìstatuslàexpired. Tiền về sau hạn vẫn được khớp.paidAtstring | nullThời điểm nhận đủ tiền.canceledAtstring | nullThời điểm hủy.createdAtstringThời điểm tạo.transactionsarrayChỉ có ở response củaGET /v1/payment-requests/:idvàPOST …/cancel: các giao dịch tiền vào đã khớp, cũ nhất trước.Các trường con
idstringID giao dịch, tiền tốtxn_.amountintegerSố tiền của giao dịch.contentstring | nullNội dung chuyển khoản ngân hàng gửi về.transactedAtstringThời điểm giao dịch theo ngân hàng.
{
"id": "pr_3fK9mQ2xLwA7nB5vR8tY1cZd",
"object": "payment_request",
"livemode": true,
"amount": 150000,
"amountReceived": 0,
"currency": "VND",
"status": "pending",
"code": "TT7K3M9Q",
"orderId": "DH1028",
"description": "Đơn hàng DH1028",
"bankAccount": {
"id": "ba_8Hn2PqR5sT7vW9xY3zA4bC6d",
"bank": {
"bin": "970422",
"name": "MB Bank"
},
"accountNumber": "0123456789",
"label": "MB Bank •••• 6789"
},
"transfer": {
"bankBin": "970422",
"accountNumber": "0123456789",
"accountName": "NGUYEN VAN AN",
"amount": 150000,
"content": "TT7K3M9Q"
},
"qr": {
"type": "static",
"payload": "00020101021238540010A00000072701240006970422011001234567890208QRIBFTTA530370454061500005802VN5913NGUYEN VAN AN62120808TT7K3M9Q6304FE1C",
"expiresAt": null
},
"hostedUrl": "https://app.bankopenapi.com/thanh-toan/pr_3fK9mQ2xLwA7nB5vR8tY1cZd",
"paymentLinkId": null,
"expiresAt": "2026-11-06T03:20:11.000Z",
"paidAt": null,
"canceledAt": null,
"createdAt": "2026-11-05T03:20:11.000Z"
}Tạo yêu cầu thanh toán
POST/v1/payment-requests
Tạo một yêu cầu ở trạng thái pending với một mã TT… mới. Vẽ qr.payload của response thành ảnh QR trong trang của bạn (hoặc dùng transfer để tự dựng VietQR), hoặc gửi hostedUrl cho người trả để dùng trang thanh toán dựng sẵn.
Body (JSON)
amountintegerbắt buộcSố tiền cần thu, từ1000đến500000000VND.orderIdstringkhông bắt buộcMã đơn hàng bên hệ thống của bạn, tối đa 64 ký tự: chữ, số và_ . : / # -. Không bắt buộc duy nhất: hai request cùngorderIdtạo hai yêu cầu.descriptionstringkhông bắt buộcMô tả ngắn, cắt còn 200 ký tự. Hiện cho người trả ở tranghostedUrl; không nằm trong nội dung chuyển khoản.expiresInMinutesintegerkhông bắt buộcThời hạn thanh toán tính từ lúc tạo, từ5đến10080phút (7 ngày). Mặc định1440(24 giờ).bankAccountIdstringkhông bắt buộcID tài khoản ngân hàng nhận tiền (ba_…, xem trong app). Bỏ trống thì dùng tài khoản liên kết gần nhất.
Lỗi
400 INVALID_AMOUNTamountthiếu, không phải số nguyên hoặc ngoài khoảng cho phép.400 INVALID_ORDER_IDorderIdquá 64 ký tự hoặc có ký tự không cho phép.400 INVALID_EXPIRYexpiresInMinutesngoài khoảng 5 đến 10080.400 INVALID_BANK_ACCOUNTbankAccountIdkhông phải tài khoản đang liên kết của bạn.422 NO_BANK_ACCOUNTChưa liên kết tài khoản ngân hàng nào.400 INVALID_JSONBody không phải JSON object.
curl -X POST https://app.bankopenapi.com/api/v1/payment-requests \
-H "Authorization: Bearer $BANKOPENAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 150000,
"orderId": "DH1028",
"description": "Đơn hàng DH1028",
"expiresInMinutes": 60
}'const res = await fetch("https://app.bankopenapi.com/api/v1/payment-requests", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BANKOPENAPI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: 150000,
orderId: "DH1028",
description: "Đơn hàng DH1028",
expiresInMinutes: 60,
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
console.log(data);<?php
$ch = curl_init('https://app.bankopenapi.com/api/v1/payment-requests');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('BANKOPENAPI_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'amount' => 150000,
'orderId' => 'DH1028',
'description' => 'Đơn hàng DH1028',
'expiresInMinutes' => 60,
]),
]);
$data = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) {
throw new Exception($data['error']['code'] . ': ' . $data['error']['message']);
}
print_r($data);import os
import requests
res = requests.post(
"https://app.bankopenapi.com/api/v1/payment-requests",
headers={"Authorization": f"Bearer {os.environ['BANKOPENAPI_API_KEY']}"},
json={
"amount": 150000,
"orderId": "DH1028",
"description": "Đơn hàng DH1028",
"expiresInMinutes": 60,
},
timeout=15,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data){
"id": "pr_3fK9mQ2xLwA7nB5vR8tY1cZd",
"object": "payment_request",
"livemode": true,
"amount": 150000,
"amountReceived": 0,
"currency": "VND",
"status": "pending",
"code": "TT7K3M9Q",
"orderId": "DH1028",
"description": "Đơn hàng DH1028",
"bankAccount": {
"id": "ba_8Hn2PqR5sT7vW9xY3zA4bC6d",
"bank": {
"bin": "970422",
"name": "MB Bank"
},
"accountNumber": "0123456789",
"label": "MB Bank •••• 6789"
},
"transfer": {
"bankBin": "970422",
"accountNumber": "0123456789",
"accountName": "NGUYEN VAN AN",
"amount": 150000,
"content": "TT7K3M9Q"
},
"qr": {
"type": "static",
"payload": "00020101021238540010A00000072701240006970422011001234567890208QRIBFTTA530370454061500005802VN5913NGUYEN VAN AN62120808TT7K3M9Q6304FE1C",
"expiresAt": null
},
"hostedUrl": "https://app.bankopenapi.com/thanh-toan/pr_3fK9mQ2xLwA7nB5vR8tY1cZd",
"paymentLinkId": null,
"expiresAt": "2026-11-05T04:20:11.000Z",
"paidAt": null,
"canceledAt": null,
"createdAt": "2026-11-05T03:20:11.000Z"
}Xem một yêu cầu
GET/v1/payment-requests/:id
Trả yêu cầu với trạng thái mới nhất, kèm mảng transactions gồm các giao dịch đã khớp. Dùng để kiểm tra lại sau khi nhận webhook, hoặc để đối soát khi báo có đang tạm dừng.
Lỗi
404 NOT_FOUNDKhông có yêu cầu với ID này trong tài khoản của bạn.
curl https://app.bankopenapi.com/api/v1/payment-requests/pr_3fK9mQ2xLwA7nB5vR8tY1cZd \
-H "Authorization: Bearer $BANKOPENAPI_API_KEY"const res = await fetch("https://app.bankopenapi.com/api/v1/payment-requests/pr_3fK9mQ2xLwA7nB5vR8tY1cZd", {
headers: {
Authorization: `Bearer ${process.env.BANKOPENAPI_API_KEY}`,
},
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
console.log(data);<?php
$ch = curl_init('https://app.bankopenapi.com/api/v1/payment-requests/pr_3fK9mQ2xLwA7nB5vR8tY1cZd');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('BANKOPENAPI_API_KEY'),
],
]);
$data = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) {
throw new Exception($data['error']['code'] . ': ' . $data['error']['message']);
}
print_r($data);import os
import requests
res = requests.get(
"https://app.bankopenapi.com/api/v1/payment-requests/pr_3fK9mQ2xLwA7nB5vR8tY1cZd",
headers={"Authorization": f"Bearer {os.environ['BANKOPENAPI_API_KEY']}"},
timeout=15,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data){
"id": "pr_3fK9mQ2xLwA7nB5vR8tY1cZd",
"object": "payment_request",
"livemode": true,
"amount": 150000,
"amountReceived": 150000,
"currency": "VND",
"status": "paid",
"code": "TT7K3M9Q",
"orderId": "DH1028",
"description": "Đơn hàng DH1028",
"bankAccount": {
"id": "ba_8Hn2PqR5sT7vW9xY3zA4bC6d",
"bank": {
"bin": "970422",
"name": "MB Bank"
},
"accountNumber": "0123456789",
"label": "MB Bank •••• 6789"
},
"transfer": {
"bankBin": "970422",
"accountNumber": "0123456789",
"accountName": "NGUYEN VAN AN",
"amount": 0,
"content": "TT7K3M9Q"
},
"qr": null,
"hostedUrl": "https://app.bankopenapi.com/thanh-toan/pr_3fK9mQ2xLwA7nB5vR8tY1cZd",
"paymentLinkId": null,
"expiresAt": "2026-11-06T03:20:11.000Z",
"paidAt": "2026-11-05T03:24:52.000Z",
"canceledAt": null,
"createdAt": "2026-11-05T03:20:11.000Z",
"transactions": [
{
"id": "txn_5Rk8Wm2Qp7Xn4Bv9Ls3Ht6Jc",
"amount": 150000,
"content": "NGUYEN VAN BINH chuyen tien TT7K3M9Q",
"transactedAt": "2026-11-05T03:24:50.000Z"
}
]
}Liệt kê yêu cầu
GET/v1/payment-requests
Trả danh sách mới nhất trước. Còn trang sau thì hasMore là true: gọi lại với starting_after là id của phần tử cuối. Phần tử trong danh sách không có mảng transactions.
Query
limitintegerkhông bắt buộcSố phần tử mỗi trang, 1 đến 100. Mặc định 50.statusenumkhông bắt buộcLọc theo nhóm trạng thái:open(pending còn hạn),paid,attention(underpaid hoặc overpaid),closed(expired hoặc canceled). Giá trị khác bị bỏ qua.order_idstringkhông bắt buộcChỉ lấy yêu cầu cóorderIdđúng bằng giá trị này.qstringkhông bắt buộcTìm theo mãTT…hoặc ID (khớp đúng), một phầnorderIdhoặcdescription, hay số tiền (chỉ gồm chữ số).starting_afterstringkhông bắt buộcID của phần tử cuối ở trang trước, để lấy trang tiếp theo.
Lỗi
400 INVALID_CURSORstarting_afterkhông phải ID yêu cầu của bạn.
curl "https://app.bankopenapi.com/api/v1/payment-requests?status=open&limit=20" \
-H "Authorization: Bearer $BANKOPENAPI_API_KEY"const res = await fetch("https://app.bankopenapi.com/api/v1/payment-requests?status=open&limit=20", {
headers: {
Authorization: `Bearer ${process.env.BANKOPENAPI_API_KEY}`,
},
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
console.log(data);<?php
$ch = curl_init('https://app.bankopenapi.com/api/v1/payment-requests?status=open&limit=20');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('BANKOPENAPI_API_KEY'),
],
]);
$data = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) {
throw new Exception($data['error']['code'] . ': ' . $data['error']['message']);
}
print_r($data);import os
import requests
res = requests.get(
"https://app.bankopenapi.com/api/v1/payment-requests",
headers={"Authorization": f"Bearer {os.environ['BANKOPENAPI_API_KEY']}"},
params={
"status": "open",
"limit": 20,
},
timeout=15,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data){
"object": "list",
"data": [
{
"id": "pr_3fK9mQ2xLwA7nB5vR8tY1cZd",
"object": "payment_request",
"livemode": true,
"amount": 150000,
"amountReceived": 0,
"currency": "VND",
"status": "pending",
"code": "TT7K3M9Q",
"orderId": "DH1028",
"description": "Đơn hàng DH1028",
"bankAccount": {
"id": "ba_8Hn2PqR5sT7vW9xY3zA4bC6d",
"bank": {
"bin": "970422",
"name": "MB Bank"
},
"accountNumber": "0123456789",
"label": "MB Bank •••• 6789"
},
"transfer": {
"bankBin": "970422",
"accountNumber": "0123456789",
"accountName": "NGUYEN VAN AN",
"amount": 150000,
"content": "TT7K3M9Q"
},
"qr": {
"type": "static",
"payload": "00020101021238540010A00000072701240006970422011001234567890208QRIBFTTA530370454061500005802VN5913NGUYEN VAN AN62120808TT7K3M9Q6304FE1C",
"expiresAt": null
},
"hostedUrl": "https://app.bankopenapi.com/thanh-toan/pr_3fK9mQ2xLwA7nB5vR8tY1cZd",
"paymentLinkId": null,
"expiresAt": "2026-11-06T03:20:11.000Z",
"paidAt": null,
"canceledAt": null,
"createdAt": "2026-11-05T03:20:11.000Z"
}
],
"hasMore": false
}Hủy yêu cầu
POST/v1/payment-requests/:id/cancel
Hủy yêu cầu chưa nhận đồng nào (pending hoặc expired), ví dụ khi khách hủy đơn. Sau khi hủy, khoản chuyển ghi mã này không còn được khớp. Không có body.
Lỗi
409 CANNOT_CANCELYêu cầu đã nhận tiền hoặc đã hủy trước đó.404 NOT_FOUNDKhông có yêu cầu với ID này trong tài khoản của bạn.
curl -X POST https://app.bankopenapi.com/api/v1/payment-requests/pr_3fK9mQ2xLwA7nB5vR8tY1cZd/cancel \
-H "Authorization: Bearer $BANKOPENAPI_API_KEY"const res = await fetch("https://app.bankopenapi.com/api/v1/payment-requests/pr_3fK9mQ2xLwA7nB5vR8tY1cZd/cancel", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BANKOPENAPI_API_KEY}`,
},
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
console.log(data);<?php
$ch = curl_init('https://app.bankopenapi.com/api/v1/payment-requests/pr_3fK9mQ2xLwA7nB5vR8tY1cZd/cancel');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('BANKOPENAPI_API_KEY'),
],
]);
$data = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) {
throw new Exception($data['error']['code'] . ': ' . $data['error']['message']);
}
print_r($data);import os
import requests
res = requests.post(
"https://app.bankopenapi.com/api/v1/payment-requests/pr_3fK9mQ2xLwA7nB5vR8tY1cZd/cancel",
headers={"Authorization": f"Bearer {os.environ['BANKOPENAPI_API_KEY']}"},
timeout=15,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data){
"id": "pr_3fK9mQ2xLwA7nB5vR8tY1cZd",
"object": "payment_request",
"livemode": true,
"amount": 150000,
"amountReceived": 0,
"currency": "VND",
"status": "canceled",
"code": "TT7K3M9Q",
"orderId": "DH1028",
"description": "Đơn hàng DH1028",
"bankAccount": {
"id": "ba_8Hn2PqR5sT7vW9xY3zA4bC6d",
"bank": {
"bin": "970422",
"name": "MB Bank"
},
"accountNumber": "0123456789",
"label": "MB Bank •••• 6789"
},
"transfer": {
"bankBin": "970422",
"accountNumber": "0123456789",
"accountName": "NGUYEN VAN AN",
"amount": 150000,
"content": "TT7K3M9Q"
},
"qr": null,
"hostedUrl": "https://app.bankopenapi.com/thanh-toan/pr_3fK9mQ2xLwA7nB5vR8tY1cZd",
"paymentLinkId": null,
"expiresAt": "2026-11-06T03:20:11.000Z",
"paidAt": null,
"canceledAt": "2026-11-05T05:02:47.000Z",
"createdAt": "2026-11-05T03:20:11.000Z",
"transactions": []
}{
"error": {
"code": "CANNOT_CANCEL",
"message": "Chỉ hủy được yêu cầu chưa nhận tiền."
}
}