Sandbox
Viết và kiểm thử tích hợp mà không cần liên kết ngân hàng hay chuyển tiền thật. Sandbox có bộ dữ liệu riêng: tài khoản ngân hàng thử, yêu cầu thanh toán, giao dịch giả lập, API key và webhook endpoint, tách hẳn khỏi dữ liệu thật.
Khác gì dữ liệu thật
- API key sandbox có dạng
boa_test_…; khóa thật làboa_live_…. Mỗi khóa chỉ thấy dữ liệu của chế độ mình. - Mọi đối tượng (
transaction,payment_request,bank_account,webhook_endpoint) và mọi sự kiện webhook có trườnglivemode:falseở sandbox. - Tài khoản ngân hàng thử (ngân hàng "Ngân hàng thử", BIN
000000) tạo ngay, không OTP, không có mã VietQR quét được. - Không tính phí, không trừ số dư, không tính vào lượt miễn phí. Kênh báo có (Telegram, email…) và thông báo trình duyệt chỉ chạy với tiền thật.
Bắt đầu
- Vào app, bật công tắc Sandbox trên thanh trên cùng.
- Trang API key: tạo khóa (khóa tạo ra là
boa_test_…). Trang Webhook: thêm endpoint nhận sự kiện thử. - Trang Tài khoản ngân hàng: bấm Thêm tài khoản thử, hoặc gọi API dưới đây.
curl -X POST https://app.bankopenapi.com/api/v1/test/bank-accounts \
-H "Authorization: Bearer $BANKOPENAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountName": "NGUYEN VAN THU"
}'const res = await fetch("https://app.bankopenapi.com/api/v1/test/bank-accounts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BANKOPENAPI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
accountName: "NGUYEN VAN THU",
}),
});
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/test/bank-accounts');
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([
'accountName' => 'NGUYEN VAN THU',
]),
]);
$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/test/bank-accounts",
headers={"Authorization": f"Bearer {os.environ['BANKOPENAPI_API_KEY']}"},
json={
"accountName": "NGUYEN VAN THU",
},
timeout=15,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data)Sau đó gọi API như bình thường với khóa boa_test_…: POST /v1/payment-requests tạo yêu cầu thanh toán thử, trả mã thanh toán và thông tin chuyển khoản của tài khoản thử.
Giả lập tiền vào
Hai endpoint chỉ dùng được với khóa boa_test_… (khóa thật nhận 400 LIVE_MODE). Cả hai chạy đúng luồng của tiền thật: ghi giao dịch, khớp yêu cầu thanh toán theo mã trong nội dung, gửi webhook.
POST /v1/test/payment-requests/:id/payvới{ amount? }: người mua thanh toán yêu cầu; mặc định chuyển đúng số còn thiếu. Gửi số nhỏ hơn để thửpayment_request.underpaid.POST /v1/test/transactionsvới{ bankAccountId?, amount, content? }: tiền vào tài khoản thử với nội dung tùy ý, vd. để thử giao dịch không khớp đơn.
curl -X POST https://app.bankopenapi.com/api/v1/test/payment-requests/pr_fbmYHwnajfwiQLTH8SOkjM2H/pay \
-H "Authorization: Bearer $BANKOPENAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
}'const res = await fetch("https://app.bankopenapi.com/api/v1/test/payment-requests/pr_fbmYHwnajfwiQLTH8SOkjM2H/pay", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BANKOPENAPI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
}),
});
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/test/payment-requests/pr_fbmYHwnajfwiQLTH8SOkjM2H/pay');
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([
]),
]);
$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/test/payment-requests/pr_fbmYHwnajfwiQLTH8SOkjM2H/pay",
headers={"Authorization": f"Bearer {os.environ['BANKOPENAPI_API_KEY']}"},
json={
},
timeout=15,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data)Trong app cũng có nút Giả lập tiền vào (trang Giao dịch) và Giả lập thanh toán (chi tiết một yêu cầu thanh toán).
Webhook ở sandbox
Sự kiện của dữ liệu thử chỉ gửi tới endpoint thêm ở sandbox, và ngược lại. Payload, chữ ký và lịch retry giống hệt dữ liệu thật; chỉ kháclivemode.
{
"id": "evt_9xQ2mT7kLp3sVr8cZn1wYb4e",
"type": "payment_request.paid",
"livemode": false,
"timestamp": "2026-11-05T09:12:03.000Z",
"data": {
"id": "pr_fbmYHwnajfwiQLTH8SOkjM2H",
"object": "payment_request",
"livemode": false,
"amount": 150000,
"amountReceived": 150000,
"status": "paid",
"code": "TTS6GNPX",
"orderId": "DH-TEST-1"
}
}Chuyển sang dữ liệu thật
- Tắt sandbox, liên kết tài khoản ngân hàng thật.
- Tạo API key
boa_live_…và webhook endpoint thật (signing secret khác secret của endpoint thử). - Thay khóa và secret trên hệ thống production. Code không cần sửa gì khác: API, payload và chữ ký giống nhau ở hai chế độ.
Lỗi thường gặp ở sandbox: xem Lỗi và mã lỗi.