Bắt đầu nhanh

Bốn bước để hệ thống của bạn tự biết đơn nào đã được chuyển khoản: tạo API key, liên kết tài khoản ngân hàng, tạo yêu cầu thanh toán, nhận webhook. Chưa có tài khoản thì đăng ký trước. Sandbox dùng ngay, không cần nạp tiền; liên kết tài khoản ngân hàng thật cần nạp lần đầu từ 5.000 ₫.

1. Tạo API key

Vào trang API key trong app, bấm "Tạo API key", đặt tên (ví dụ "Web bán hàng (production)"). Khóa có dạng boa_live_ + 36 ký tự và chỉ hiện đúng một lần: sao chép ngay vào biến môi trường của hệ thống.

Terminal
export BANKOPENAPI_API_KEY="boa_live_..."

Gọi thử endpoint tài khoản để kiểm tra khóa:

GET /v1/account
curl https://app.bankopenapi.com/api/v1/account \
  -H "Authorization: Bearer $BANKOPENAPI_API_KEY"
Response 200
{
  "object": "account",
  "id": "Xq7oT2mB9kLr4VnC8pWzA1sD3fGhJ6uE",
  "email": "dev@example.com",
  "name": "Nguyễn Văn An",
  "createdAt": "2026-11-04T08:15:30.000Z"
}

Ví dụ curl viết cho bash, zsh (macOS, Linux, WSL). Khóa chỉ dùng từ backend: đừng đặt trong code chạy trên trình duyệt hay app di động. Xem Xác thực.

2. Liên kết tài khoản ngân hàng

Vào trang Tài khoản ngân hàng, chọn ngân hàng, nhập thông tin tài khoản và OTP ngân hàng gửi về số điện thoại của chủ tài khoản. Liên kết xong, mọi khoản tiền vào tài khoản này đều được BankOpenAPI ghi nhận.

Bước này làm trong app vì chủ tài khoản phải xác nhận bằng OTP; muốn thử trước thì bật Sandbox và thêm tài khoản thử. Chưa có tài khoản nào thì request tạo yêu cầu thanh toán trả lỗi 422 NO_BANK_ACCOUNT. Danh sách ngân hàng ở trang Ngân hàng hỗ trợ.

3. Tạo yêu cầu thanh toán

Khi khách đặt đơn, hệ thống của bạn tạo một yêu cầu thanh toán với số tiền và mã đơn của bạn. Không gửi bankAccountId thì BankOpenAPI dùng tài khoản liên kết gần nhất.

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"
  }'
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-06T03:20:11.000Z",
  "paidAt": null,
  "canceledAt": null,
  "createdAt": "2026-11-05T03:20:11.000Z"
}

Lưu id vào đơn hàng. Trường transfer là mọi thứ người trả cần: chuyển transfer.amount tới tài khoản transfer.accountNumber (ngân hàng có mã BIN transfer.bankBin), ghi transfer.content trong nội dung. Dựng mã VietQR từ các trường này để app ngân hàng của người trả điền sẵn tất cả: xem VietQR và nội dung chuyển khoản.

Chưa muốn tự dựng trang thanh toán? Gửi hostedUrl cho người trả: trang do BankOpenAPI dựng sẵn, có mã VietQR, thông tin chuyển khoản và tự báo khi nhận tiền.

4. Nhận webhook

Vào trang Webhook, bấm "Thêm endpoint", nhập URL https:// của hệ thống và chọn sự kiện payment_request.paid. Sao chép signing secret (dạng whsec_...) vào biến môi trường BANKOPENAPI_WEBHOOK_SECRET.

Endpoint của bạn verify chữ ký bằng thư viện standardwebhooks (có cho Node.js, PHP, Python và nhiều ngôn ngữ khác), chống xử lý trùng theo event.id, rồi trả 200:

Endpoint nhận webhook
import express from "express";
import { Webhook } from "standardwebhooks";

const app = express();
// Signing secret của endpoint (dạng "whsec_..."), lấy ở trang Webhook trong app
const wh = new Webhook(process.env.BANKOPENAPI_WEBHOOK_SECRET);

// express.raw giữ nguyên body thô: verify phải chạy trên đúng chuỗi BankOpenAPI đã ký
app.post("/webhooks/bankopenapi", express.raw({ type: "application/json" }), async (req, res) => {
  let event;
  try {
    event = wh.verify(req.body, req.headers);
  } catch {
    return res.status(400).send("Invalid signature");
  }

  // Chống xử lý trùng: một sự kiện giữ nguyên event.id qua mọi lần retry
  if (await alreadyProcessed(event.id)) return res.sendStatus(200);

  if (event.type === "payment_request.paid") {
    await markOrderPaid(event.data.orderId, event.data);
  }

  await markProcessed(event.id);
  res.sendStatus(200);
});

app.listen(3000);

Khi đơn nhận đủ tiền, endpoint nhận body như sau:

payment_request.paid
{
  "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"
  }
}

Bấm "Gửi thử" trên trang Webhook để nhận sự kiện webhook.test. Đang code trên máy, dùng một tunnel để có URL https công khai, ví dụ cloudflared tunnel --url http://localhost:3000.

Thử thật từ đầu đến cuối: tạo yêu cầu 1.000 VND, quét mã VietQR và tự chuyển khoản. Vài giây sau endpoint nhận transaction.received và payment_request.paid.

Bước tiếp theo

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.