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ĩaWebhook
pendingChờ thanh toán: chưa nhận tiền, còn hạn.Không có
paidNhận đủ tiền.payment_request.paid
overpaidNhậ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
underpaidNhậ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
expiredQuá 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.type là 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.accountNumber với nội dung transfer.content.
  • Yêu cầu đã nhận một phần (underpaid) thì qr chuyể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/overpaid vẫn có thể xảy ra.

Đối tượng payment_request

Các trường

  • idstring
    ID của yêu cầu, tiền tố pr_.
  • objectstring
    Luôn là "payment_request".
  • livemodeboolean
    false với yêu cầu tạo bằng khóa boa_test_… (sandbox).
  • amountinteger
    Số tiền cần thu, đơn vị VND.
  • amountReceivedinteger
    Tổng số tiền đã nhận và khớp với yêu cầu này.
  • currencystring
    Luôn là "VND".
  • statusenum
    pending, paid, underpaid, overpaid, expired hoặc canceled. Xem Trạng thái.
  • codestring
    Mã 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 | null
    Mã đơn hàng bên hệ thống của bạn, như lúc tạo.
  • descriptionstring | null
    Mô tả ngắn như lúc tạo, hiện cho người trả ở trang thanh toán (hostedUrl).
  • bankAccountobject
    Tài khoản ngân hàng nhận tiền của yêu cầu.
    Các trường con
    • idstring
      ID tài khoản ngân hàng, tiền tố ba_.
    • bank.binstring
      Mã BIN 6 chữ số của ngân hàng (chuẩn NAPAS).
    • bank.namestring | null
      Tên ngắn của ngân hàng, ví dụ MB Bank.
    • accountNumberstring
      Số tài khoản đã liên kết.
    • labelstring
      Nhãn để hiển thị, che bớt số tài khoản: MB Bank •••• 6789.
  • transferobject
    Thông tin chuyển khoản cho người trả, dùng để dựng mã VietQR.
    Các trường con
    • bankBinstring
      Mã BIN ngân hàng nhận.
    • accountNumberstring
      Tà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ác bankAccount.accountNumber. Luôn dùng số này khi dựng QR.
    • accountNamestring
      Tên chủ tài khoản, in hoa không dấu.
    • amountinteger
      Số tiền còn phải chuyển: amount − amountReceived, không nhỏ hơn 0.
    • contentstring
      Nội dung chuyển khoản, chính là code.
  • qrobject | null
    Mã QR cho người trả quét, đã dựng sẵn. null khi yêu cầu không còn nhận tiền (paid, overpaid, expired, canceled). Xem QR động.
    Các trường con
    • typeenum
      dynamic: 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ới transfer.accountNumber, khớp theo mã TT… trong nội dung.
    • payloadstring
      Chuỗ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 | null
      Hạn của QR động, bằng expiresAt của yêu cầu. null với VietQR tĩnh.
  • hostedUrlstring
    Trang 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 | null
    Link 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). null với yêu cầu tạo bằng API hoặc trong app.
  • expiresAtstring
    Hạn thanh toán. Quá hạn mà chưa nhận tiền thì status là expired. Tiền về sau hạn vẫn được khớp.
  • paidAtstring | null
    Thời điểm nhận đủ tiền.
  • canceledAtstring | null
    Thời điểm hủy.
  • createdAtstring
    Thời điểm tạo.
  • transactionsarray
    Chỉ có ở response của GET /v1/payment-requests/:id và POST …/cancel: các giao dịch tiền vào đã khớp, cũ nhất trước.
    Các trường con
    • idstring
      ID giao dịch, tiền tố txn_.
    • amountinteger
      Số tiền của giao dịch.
    • contentstring | null
      Nội dung chuyển khoản ngân hàng gửi về.
    • transactedAtstring
      Thời điểm giao dịch theo ngân hàng.
Đối tượng payment_request
{
  "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ộc
    Số tiền cần thu, từ 1000 đến 500000000 VND.
  • orderIdstringkhông bắt buộc
    Mã đơ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ùng orderId tạo hai yêu cầu.
  • descriptionstringkhông bắt buộc
    Mô tả ngắn, cắt còn 200 ký tự. Hiện cho người trả ở trang hostedUrl; không nằm trong nội dung chuyển khoản.
  • expiresInMinutesintegerkhông bắt buộc
    Thời hạn thanh toán tính từ lúc tạo, từ 5 đến 10080 phút (7 ngày). Mặc định 1440 (24 giờ).
  • bankAccountIdstringkhông bắt buộc
    ID 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_AMOUNT
    amount thiếu, không phải số nguyên hoặc ngoài khoảng cho phép.
  • 400 INVALID_ORDER_ID
    orderId quá 64 ký tự hoặc có ký tự không cho phép.
  • 400 INVALID_EXPIRY
    expiresInMinutes ngoài khoảng 5 đến 10080.
  • 400 INVALID_BANK_ACCOUNT
    bankAccountId không phải tài khoản đang liên kết của bạn.
  • 422 NO_BANK_ACCOUNT
    Chưa liên kết tài khoản ngân hàng nào.
  • 400 INVALID_JSON
    Body không phải JSON object.
POST /v1/payment-requests
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
  }'
Response 201
{
  "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_FOUND
    Không có yêu cầu với ID này trong tài khoản của bạn.
GET /v1/payment-requests/:id
curl https://app.bankopenapi.com/api/v1/payment-requests/pr_3fK9mQ2xLwA7nB5vR8tY1cZd \
  -H "Authorization: Bearer $BANKOPENAPI_API_KEY"
Response 200
{
  "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ộc
    Số phần tử mỗi trang, 1 đến 100. Mặc định 50.
  • statusenumkhông bắt buộc
    Lọ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ộc
    Chỉ lấy yêu cầu có orderId đúng bằng giá trị này.
  • qstringkhông bắt buộc
    Tìm theo mã TT… hoặc ID (khớp đúng), một phần orderId hoặc description, hay số tiền (chỉ gồm chữ số).
  • starting_afterstringkhông bắt buộc
    ID của phần tử cuối ở trang trước, để lấy trang tiếp theo.

Lỗi

  • 400 INVALID_CURSOR
    starting_after không phải ID yêu cầu của bạn.
GET /v1/payment-requests
curl "https://app.bankopenapi.com/api/v1/payment-requests?status=open&limit=20" \
  -H "Authorization: Bearer $BANKOPENAPI_API_KEY"
Response 200
{
  "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_CANCEL
    Yêu cầu đã nhận tiền hoặc đã hủy trước đó.
  • 404 NOT_FOUND
    Không có yêu cầu với ID này trong tài khoản của bạn.
POST /v1/payment-requests/:id/cancel
curl -X POST https://app.bankopenapi.com/api/v1/payment-requests/pr_3fK9mQ2xLwA7nB5vR8tY1cZd/cancel \
  -H "Authorization: Bearer $BANKOPENAPI_API_KEY"
Response 200
{
  "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": []
}
Response 409
{
  "error": {
    "code": "CANNOT_CANCEL",
    "message": "Chỉ hủy được yêu cầu chưa nhận tiền."
  }
}

Bắt đầu với BankOpenAPI

Đăng ký tài khoản, liên kết tài khoản ngân hàng và nhận webhook báo có đầu tiên. Cần tư vấn thì nhắn chúng tôi.