Sự kiện và payload
Mỗi webhook có dạng { "id", "type", "livemode", "timestamp", "data" }; livemode là false với sự kiện của sandbox. type cho biết chuyện gì xảy ra, data là đối tượng liên quan ở thời điểm đó, cùng định dạng với API.
Danh sách sự kiện
| type | Khi nào | data |
|---|---|---|
transaction.received | Có tiền vào một tài khoản ngân hàng đã liên kết (báo có). | transaction |
payment_request.paid | Yêu cầu thanh toán nhận đủ tiền (status paid, hoặc overpaid nếu chuyển thừa). | payment_request |
payment_request.underpaid | Yêu cầu thanh toán nhận được tiền nhưng chưa đủ. | payment_request |
bank_account.linked | Liên kết xong một tài khoản ngân hàng. | bank_account |
bank_account.removed | Một tài khoản ngân hàng bị hủy liên kết. | bank_account |
webhook.test | Sự kiện thử khi bấm "Gửi thử" trên trang Webhook. Chỉ gửi tới endpoint được thử. | object |
Một khoản chuyển khớp yêu cầu thanh toán sinh ra hai sự kiện: transaction.received và payment_request.paid (hoặc underpaid). Đăng ký loại nào thì nhận loại đó. Hai sự kiện có thể tới theo thứ tự bất kỳ.
transaction.received
Có tiền vào một tài khoản ngân hàng đã liên kết, kể cả khoản không ghi mã TT… nào. Hợp với hệ thống tự khớp tiền theo cách riêng, hoặc để báo có cho team. Chỉ có giao dịch tiền vào; tiền ra không sinh sự kiện.
JSON Schema: transaction.received.json
data: đối tượng transaction
idstringID giao dịch trong BankOpenAPI, tiền tốtxn_.objectstringLuôn là"transaction".livemodebooleanfalsevới giao dịch giả lập của sandbox.typestring"credit"(tiền vào) hoặc"debit"(tiền ra). Sự kiệntransaction.receivedluôn là"credit".amountintegerSố tiền, đơn vị VND.currencystringLuôn là"VND".contentstring | nullNội dung chuyển khoản ngân hàng gửi về. Ngân hàng thường thêm tên người chuyển, đổi chữ hoa, bỏ dấu. Có ngân hàng không gửi nội dung.referencestringMã tham chiếu giao dịch phía ngân hàng.receivingNumberstring | nullTài khoản nhận báo có: virtual account hoặc số tài khoản.paymentRequestIdstring | nullID yêu cầu thanh toán đã khớp với giao dịch,nullnếu không khớp yêu cầu nào.notifiedAtstring | nullThời điểm BankOpenAPI đã báo có giao dịch (webhook, kênh báo có).nullnếu chưa báo, ví dụ tiền về lúc báo có tạm dừng.bankAccountobjectTài khoản nhận tiền:id,bank.bin,bank.name,accountNumber,label.transactedAtstringThời điểm giao dịch theo ngân hàng.createdAtstringThời điểm BankOpenAPI ghi nhận giao dịch.
{
"id": "evt_2Gc7Vn4Kp9Wq3Xr8Ty5Bm1Hd",
"type": "transaction.received",
"livemode": true,
"timestamp": "2026-11-05T03:24:51.000Z",
"data": {
"id": "txn_5Rk8Wm2Qp7Xn4Bv9Ls3Ht6Jc",
"object": "transaction",
"livemode": true,
"type": "credit",
"amount": 150000,
"currency": "VND",
"content": "NGUYEN VAN BINH chuyen tien TT7K3M9Q",
"reference": "FT25309871234567",
"receivingNumber": "0123456789",
"paymentRequestId": "pr_3fK9mQ2xLwA7nB5vR8tY1cZd",
"notifiedAt": null,
"bankAccount": {
"id": "ba_8Hn2PqR5sT7vW9xY3zA4bC6d",
"bank": {
"bin": "970422",
"name": "MB Bank"
},
"accountNumber": "0123456789",
"label": "MB Bank •••• 6789"
},
"transactedAt": "2026-11-05T03:24:50.000Z",
"createdAt": "2026-11-05T03:24:51.000Z"
}
}payment_request.paid
Yêu cầu thanh toán đã nhận đủ tiền. data.status là paid, hoặc overpaid khi người trả chuyển thừa (amountReceived lớn hơn amount). Đây là sự kiện để chuyển đơn sang "đã thanh toán".
JSON Schema: payment_request.paid.json
data là đối tượng payment_request, không kèm mảng transactions. Tìm đơn của bạn theo data.id (đã lưu lúc tạo) hoặc data.orderId.
{
"id": "evt_2Gc7Vn4Kp9Wq3Xr8Ty5Bm1Hd",
"type": "payment_request.paid",
"livemode": true,
"timestamp": "2026-11-05T03:24:52.000Z",
"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"
}
}payment_request.underpaid
Yêu cầu nhận được tiền nhưng chưa đủ. data.transfer.amount là số còn thiếu: hiện lại mã VietQR với số này để người trả chuyển thêm (cùng mã). Khi đủ, bạn nhận tiếp payment_request.paid.
JSON Schema: payment_request.underpaid.json
{
"id": "evt_2Gc7Vn4Kp9Wq3Xr8Ty5Bm1Hd",
"type": "payment_request.underpaid",
"livemode": true,
"timestamp": "2026-11-05T03:24:52.000Z",
"data": {
"id": "pr_3fK9mQ2xLwA7nB5vR8tY1cZd",
"object": "payment_request",
"livemode": true,
"amount": 150000,
"amountReceived": 100000,
"currency": "VND",
"status": "underpaid",
"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": 50000,
"content": "TT7K3M9Q"
},
"qr": {
"type": "static",
"payload": "00020101021238540010A00000072701240006970422011001234567890208QRIBFTTA53037045405500005802VN5913NGUYEN VAN AN62120808TT7K3M9Q6304C187",
"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"
}
}bank_account.linked, bank_account.removed
Liên kết xong hoặc hủy liên kết một tài khoản ngân hàng (trong app). Dùng để cập nhật danh sách tài khoản nhận tiền trong hệ thống của bạn, ví dụ lưu data.id để truyền vào bankAccountId.
JSON Schema: bank_account.linked.json, bank_account.removed.json
data: đối tượng bank_account
idstringID tài khoản ngân hàng, tiền tốba_. Dùng làmbankAccountIdkhi tạo yêu cầu thanh toán.objectstringLuôn là"bank_account".livemodebooleanfalsevới tài khoản ngân hàng thử của sandbox.bankobjectbin(mã BIN NAPAS),code(mã viết tắt, vd.MBB),name.accountTypeenumpersonal-account(cá nhân),business-household-account(hộ kinh doanh) hoặcbusiness-account(doanh nghiệp).accountNumberstringSố tài khoản.accountNamestringTên chủ tài khoản, in hoa không dấu.receivingNumberstring | nullTài khoản nhận báo có: virtual account (tài khoản định danh) hoặc chính số tài khoản.notifyRealAccountbooleantruenếu nhận báo có ở số tài khoản gốc thay vì virtual account; khi đóreceivingNumberlà số tài khoản gốc.statusstringactive(đang liên kết) hoặcremoved(đã hủy liên kết, sự kiệnbank_account.removed).pendingStepstring | nullBước xác nhận còn dang dở;nullkhi đã liên kết xong.linkedAtstring | nullThời điểm liên kết xong.createdAtstringThời điểm bắt đầu liên kết.
{
"id": "evt_2Gc7Vn4Kp9Wq3Xr8Ty5Bm1Hd",
"type": "bank_account.linked",
"livemode": true,
"timestamp": "2026-11-04T09:02:13.000Z",
"data": {
"id": "ba_8Hn2PqR5sT7vW9xY3zA4bC6d",
"object": "bank_account",
"livemode": true,
"bank": {
"bin": "970422",
"code": "MBB",
"name": "MB Bank"
},
"accountType": "personal-account",
"accountNumber": "0123456789",
"accountName": "NGUYEN VAN AN",
"receivingNumber": "0123456789",
"notifyRealAccount": false,
"status": "active",
"pendingStep": null,
"linkedAt": "2026-11-04T09:02:13.000Z",
"createdAt": "2026-11-04T09:00:41.000Z"
}
}{
"id": "evt_2Gc7Vn4Kp9Wq3Xr8Ty5Bm1Hd",
"type": "bank_account.removed",
"livemode": true,
"timestamp": "2026-11-20T02:11:40.000Z",
"data": {
"id": "ba_8Hn2PqR5sT7vW9xY3zA4bC6d",
"object": "bank_account",
"livemode": true,
"bank": {
"bin": "970422",
"code": "MBB",
"name": "MB Bank"
},
"accountType": "personal-account",
"accountNumber": "0123456789",
"accountName": "NGUYEN VAN AN",
"receivingNumber": "0123456789",
"notifyRealAccount": false,
"status": "removed",
"pendingStep": null,
"linkedAt": "2026-11-04T09:02:13.000Z",
"createdAt": "2026-11-04T09:00:41.000Z"
}
}webhook.test
Gửi khi bạn bấm "Gửi thử" trên trang Webhook, chỉ tới đúng endpoint đó, ký bằng secret của endpoint như sự kiện thật. Không chọn được loại này khi đăng ký. Endpoint của bạn chỉ cần verify và trả 200.
JSON Schema: webhook.test.json
{
"id": "evt_2Gc7Vn4Kp9Wq3Xr8Ty5Bm1Hd",
"type": "webhook.test",
"livemode": true,
"timestamp": "2026-11-05T02:58:03.000Z",
"data": {
"message": "Sự kiện thử từ BankOpenAPI. Endpoint của bạn đã nhận và verify được webhook.",
"endpointId": "we_6Tz3Nq8Lk2Vb5Xm9Rc4Wp7Jh"
}
}JSON Schema
Mỗi loại sự kiện có một JSON Schema (draft 2020-12) mô tả đúng body BankOpenAPI gửi, dùng để kiểm payload trong test, sinh kiểu dữ liệu (TypeScript, Go, Python…) hoặc đưa cho AI agent viết code xử lý webhook. Danh sách:index.json.
transaction.received.jsonpayment_request.paid.jsonpayment_request.underpaid.jsonbank_account.linked.jsonbank_account.removed.jsonwebhook.test.json
Schema cho phép trường mới (additionalProperties), đúng cam kết ở trên: đừng bật kiểm tra chặt hơn trên production, nếu không payload có thêm trường sẽ bị từ chối.
// npm install ajv ajv-formats
import Ajv2020 from "ajv/dist/2020.js";
import addFormats from "ajv-formats";
const ajv = new Ajv2020();
addFormats(ajv);
const schema = await (await fetch("https://bankopenapi.com/tai-lieu/webhook/schemas/transaction.received.json")).json();
const validate = ajv.compile(schema);
// event: body đã verify chữ ký (JSON.parse)
if (!validate(event)) console.error(validate.errors);Xem lại sự kiện qua API
Mọi sự kiện được lưu 30 ngày, kể cả khi chưa có endpoint nào nhận. Dùng để đối chiếu khi hệ thống của bạn lỡ mất webhook, hoặc gửi lại một sự kiện. Cùng dữ liệu với tab "Sự kiện" trên trang Webhook.
GET /v1/events: danh sách, mới nhất trước. Lọc?type=, phân tranglimitvàstarting_after. Mỗi sự kiện códeliveries: số endpoint đã nhận, đang chờ, thất bại.GET /v1/events/:id: một sự kiện kèm mảngdeliveries(từng endpoint, trạng thái, mã HTTP).POST /v1/events/:id/resendvới{ "endpointId": "we_…" }: gửi sự kiện tới một endpoint đang bật ngay lúc gọi, một lần. Cần API key toàn quyền.
curl "https://app.bankopenapi.com/api/v1/events?type=transaction.received&limit=20" \
-H "Authorization: Bearer $BANKOPENAPI_API_KEY"