Liên kết ngân hàng
Kết nối ngân hàng (bank connection) là liên kết giữa Hạ Tầng và tài khoản ngân hàng khách hàng chuyển tiền vào. Hạ Tầng chỉ đọc giao dịch tiền vào của tài khoản, không bao giờ chuyển tiền đi. Trang này hướng dẫn liên kết từng ngân hàng, cấu hình khớp mã, hủy liên kết, và nhập sao kê khi ngân hàng bỏ sót giao dịch.
Trước khi bắt đầu
- API key có
payments:write, hoặc quyền vào Cài đặt thanh toán → Tài khoản ngân hàng trên dashboard. - Với ngân hàng thật: số tài khoản, tên chủ tài khoản đúng như ngân hàng ghi nhận, và (với MB Bank) CCCD hoặc mã số thuế dùng khi mở tài khoản.
- Chọn đúng môi trường. Ngân hàng thật chỉ có ở
production; ngân hàng giả lập chỉ có ởdevelopment. Chọn sai sẽ nhận403 environment_mismatch.
Endpoint
| Method | Endpoint | Chức năng |
|---|---|---|
GET | /payments/bank-connections | Danh sách tài khoản đã liên kết (bỏ qua tài khoản đã hủy) |
POST | /payments/bank-connections | Bắt đầu liên kết |
GET | /payments/bank-connections/{id} | Chi tiết |
PATCH | /payments/bank-connections/{id} | Đổi nhãn hoặc cấu hình khớp mã |
POST | /payments/bank-connections/{id}/confirm | Xác nhận liên kết bằng mã của ngân hàng |
POST | /payments/bank-connections/{id}/unlink | Bắt đầu hủy liên kết |
POST | /payments/bank-connections/{id}/unlink/confirm | Xác nhận hủy liên kết bằng mã của ngân hàng |
POST | /payments/statements | Nhập sao kê (CSV) |
GET | /payments/statements/{id} | Tiến độ nhập sao kê |
Endpoint đọc cần payments:read, endpoint thay đổi cần payments:write.
Cách liên kết từng ngân hàng
| Ngân hàng | provider | link_mode | Bạn cần làm |
|---|---|---|---|
| MB Bank | mbbank | otp | Gửi thông tin tài khoản; MB nhắn mã tới số điện thoại đăng ký của tài khoản; xác nhận bằng mã đó |
| BIDV | bidv | bank_registered | Đăng ký dịch vụ thông báo biến động số dư của BIDV cho tài khoản tại BIDV, rồi gửi số tài khoản. Kết nối kích hoạt khi BIDV gửi tin đầu tiên cho tài khoản |
| ACB | acb | redirect | Gửi số tài khoản, rồi mở challenge.authorize_url để chủ tài khoản đồng ý trên trang của ACB. Họ được đưa về dashboard và tài khoản chuyển sang hoạt động |
| Pay2S | pay2s | bank_registered | Liên kết tài khoản trên Pay2S, tạo Hook trên đó trỏ tới webhook_url, rồi gửi số tài khoản, bank_code và token của Hook. Kết nối kích hoạt khi có giao dịch đầu tiên |
| Ngân hàng giả lập | simulator | otp | Xác nhận bằng mã 000000 |
Liên kết tài khoản MB Bank
1. Bắt đầu liên kết
- cURL
- Node.js
- Python
- PHP
curl https://api.hatang.dev/payments/bank-connections \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "mbbank",
"account_number": "910452658",
"account_name": "NGUYEN VAN A",
"account_type": "personal",
"holder_id": "001099012345",
"label": "Tài khoản bán hàng"
}'
const { data: conn, error } = await hatang.payments.bankConnections.create({
provider: "mbbank",
account_number: "910452658",
account_name: "NGUYEN VAN A",
account_type: "personal",
holder_id: "001099012345",
label: "Tài khoản bán hàng",
});
if (error) throw new Error(`${error.name}: ${error.message}`);
conn = requests.post("https://api.hatang.dev/payments/bank-connections", headers=HEADERS, json={
"provider": "mbbank",
"account_number": "910452658",
"account_name": "NGUYEN VAN A",
"account_type": "personal",
"holder_id": "001099012345",
"label": "Tài khoản bán hàng",
}, timeout=30).json()
$conn = json_decode($client->post('/payments/bank-connections', ['json' => [
'provider' => 'mbbank',
'account_number' => '910452658',
'account_name' => 'NGUYEN VAN A',
'account_type' => 'personal',
'holder_id' => '001099012345',
'label' => 'Tài khoản bán hàng',
]])->getBody(), true);
| Trường | Mô tả |
|---|---|
provider | Bắt buộc. mbbank, bidv, acb hoặc pay2s ở production; simulator ở development |
account_number | Bắt buộc với ngân hàng thật. Chữ số (Pay2S cho phép cả chữ in hoa), 4 đến 30 ký tự. Ngân hàng giả lập tự sinh nếu bạn bỏ trống |
account_name | Tên chủ tài khoản đúng như ngân hàng ghi nhận. Bắt buộc với MB Bank |
account_type | personal (mặc định) hoặc business |
holder_id | CCCD (cá nhân) hoặc mã số thuế (doanh nghiệp) dùng khi mở tài khoản. Bắt buộc với MB Bank; được lưu mã hóa |
bank_code | Với nhà cung cấp phục vụ nhiều ngân hàng (Pay2S, ngân hàng giả lập): tài khoản ở ngân hàng nào, vd BIDV, MB, VCB. Quyết định ngân hàng ghi trên VietQR. Các nhà cung cấp khác sẽ từ chối trường này |
webhook_token | Chỉ dùng và bắt buộc với Pay2S: token của Hook bạn tạo trên Pay2S |
label | Tên bạn đặt cho tài khoản, tối đa 100 ký tự |
config | Cấu hình khớp mã; xem Cấu hình khớp mã |
Phản hồi là 201 với kết nối ở pending_otp kèm challenge:
{
"object": "bank_connection",
"id": "bcn_01a0b90428fd7c499145a12c133abe41",
"environment": "production",
"provider": "mbbank",
"bank": { "bin": "970422", "name": "MB Bank" },
"account_number": "910452658",
"account_name": "NGUYEN VAN A",
"account_type": "personal",
"status": "pending_otp",
"label": "Tài khoản bán hàng",
"config": {
"code_prefix": "HT",
"payment_code_pattern": null,
"transaction_events": { "direction": "in", "require_code": false, "min_amount": 0 }
},
"capabilities": { "pushNotifications": true, "history": false, "accountInfo": false, "virtualAccounts": false, "refunds": false },
"link_mode": "otp",
"webhook_url": null,
"consent": null,
"last_transaction_at": null,
"activated_at": null,
"revoked_at": null,
"created_at": "2026-09-19T09:29:51.402Z",
"challenge": { "id": "lch_01a0b90428e17d2c8b5f0a4e3c2d1b0a", "expires_at": "2026-09-19T09:34:51.402Z", "delivery": "sms_otp" }
}
challenge.delivery cho biết bước tiếp theo: sms_otp (mã đang được gửi), redirect (mở challenge.authorize_url) hoặc bank_registration (chờ tin đầu tiên của ngân hàng).
2. Xác nhận bằng mã
- cURL
- Node.js
- Python
- PHP
curl https://api.hatang.dev/payments/bank-connections/bcn_123/confirm \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "otp": "123456" }'
const { data, error } = await hatang.payments.bankConnections.confirm(conn.id, "123456");
requests.post(f"https://api.hatang.dev/payments/bank-connections/{conn['id']}/confirm",
headers=HEADERS, json={"otp": "123456"}, timeout=30).raise_for_status()
$client->post("/payments/bank-connections/{$conn['id']}/confirm", ['json' => ['otp' => '123456']]);
otp gồm 4 đến 8 chữ số. Khi thành công, kết nối chuyển sang active và webhook bank_connection.activated được gửi. Mỗi lần nhập sai được tính một lượt; sau 5 lượt sai, mã hết hiệu lực và bạn phải bắt đầu lại.
Liên kết tài khoản BIDV, ACB hoặc Pay2S
- BIDV. Đăng ký dịch vụ thông báo biến động số dư cho tài khoản tại BIDV, rồi
POSTvớiprovider: "bidv"vàaccount_number. Kết nối ở trạng tháipending_otp(dashboard hiển thị "Chờ ngân hàng") cho tới khi BIDV gửi tin đầu tiên cho tài khoản, lúc đó kết nối được kích hoạt. Gọiconfirmsẽ nhận409 invalid_state. - ACB.
POSTvớiprovider: "acb"vàaccount_number, rồi đưa chủ tài khoản tớichallenge.authorize_urltrong vài phút. Sau khi họ đồng ý trên trang của ACB, ACB đưa họ về Cài đặt thanh toán → Tài khoản ngân hàng và tài khoản chuyển sangactive. Sự đồng ý này cho phép đọc lịch sử tài khoản, có hiệu lực tối đa 180 ngày và hiển thị ở trườngconsentcủa kết nối. ACB không đẩy giao dịch; Hạ Tầng đọc lịch sử mỗi 10 phút. - Pay2S. Liên kết tài khoản trên Pay2S trước. Lấy
webhook_urltừ một kết nối Pay2S bất kỳ (hoặc từ form trên dashboard), tạo Hook trên Pay2S trỏ tới URL đó, và sao chép token của Hook. Sau đóPOSTvớiprovider: "pay2s",bank_code,account_number(đúng như Pay2S ghi) vàwebhook_token. Kết nối kích hoạt khi Pay2S đẩy giao dịch đầu tiên của tài khoản. Pay2S đọc được tài khoản tạiVCB,ICB,TCB,BIDV,ACB,MBvàTPB.
Trạng thái
status | Ý nghĩa |
|---|---|
pending_otp | Đang chờ: chờ mã, chờ chủ tài khoản đồng ý, hoặc chờ tin đầu tiên của ngân hàng |
active | Đang nhận giao dịch |
needs_reauth | Ngân hàng yêu cầu liên kết lại |
revoked | Đã hủy liên kết |
error | Ngân hàng báo lỗi với liên kết |
Cấu hình khớp mã (config)
Đổi bằng PATCH, hoặc trên dashboard tại Cài đặt thanh toán → Tài khoản ngân hàng → Khớp lệnh và thông báo. Chỉ những trường bạn gửi mới thay đổi.
- cURL
- Node.js
- Python
- PHP
curl -X PATCH https://api.hatang.dev/payments/bank-connections/bcn_123 \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"label": "Cửa hàng chính",
"config": {
"code_prefix": "SHOP",
"payment_code_pattern": { "prefix": "DH", "min_length": 4, "max_length": 8, "charset": "digits" },
"transaction_events": { "direction": "in", "require_code": true, "min_amount": 10000 }
}
}'
await hatang.payments.bankConnections.update("bcn_123", {
label: "Cửa hàng chính",
config: {
code_prefix: "SHOP",
payment_code_pattern: { prefix: "DH", min_length: 4, max_length: 8, charset: "digits" },
transaction_events: { direction: "in", require_code: true, min_amount: 10_000 },
},
});
requests.patch("https://api.hatang.dev/payments/bank-connections/bcn_123", headers=HEADERS, json={
"label": "Cửa hàng chính",
"config": {
"code_prefix": "SHOP",
"payment_code_pattern": {"prefix": "DH", "min_length": 4, "max_length": 8, "charset": "digits"},
"transaction_events": {"direction": "in", "require_code": True, "min_amount": 10000},
},
}, timeout=10).raise_for_status()
$client->patch('/payments/bank-connections/bcn_123', ['json' => [
'label' => 'Cửa hàng chính',
'config' => [
'code_prefix' => 'SHOP',
'payment_code_pattern' => ['prefix' => 'DH', 'min_length' => 4, 'max_length' => 8, 'charset' => 'digits'],
'transaction_events' => ['direction' => 'in', 'require_code' => true, 'min_amount' => 10000],
],
]]);
| Trường | Mặc định | Mô tả |
|---|---|---|
code_prefix | HT | Tiền tố của mã thanh toán tự sinh, 2 đến 5 chữ in hoa (luồng đơn hàng) |
payment_code_pattern | null | Định dạng mã đơn của bạn, được tìm trong mọi giao dịch (luồng chỉ nhận giao dịch). null để tắt |
payment_code_pattern.prefix | – | 1 đến 10 chữ in hoa hoặc chữ số |
payment_code_pattern.min_length / max_length | 3 / 10 | Độ dài phần sau tiền tố, từ 1 đến 30 |
payment_code_pattern.charset | digits | digits hoặc alnum (chữ và số) |
transaction_events.direction | in | in, hoặc all để báo cả tiền ra |
transaction_events.require_code | false | Chỉ báo giao dịch tìm thấy mã hoặc khớp với một payment |
transaction_events.min_amount | 0 | Không báo giao dịch dưới số tiền này (VND) |
transaction_events chỉ lọc transaction.received. Payment luôn được khớp và báo.
Hủy liên kết
- cURL
- Node.js
- Python
- PHP
curl -X POST https://api.hatang.dev/payments/bank-connections/bcn_123/unlink \
-H "Authorization: Bearer $HATANG_API_KEY"
# Chỉ khi phản hồi có challenge:
curl https://api.hatang.dev/payments/bank-connections/bcn_123/unlink/confirm \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "otp": "123456" }'
const { data } = await hatang.payments.bankConnections.unlink("bcn_123");
if (data?.status !== "revoked") await hatang.payments.bankConnections.confirmUnlink("bcn_123", "123456");
conn = requests.post("https://api.hatang.dev/payments/bank-connections/bcn_123/unlink",
headers=HEADERS, timeout=30).json()
if conn.get("challenge"):
requests.post("https://api.hatang.dev/payments/bank-connections/bcn_123/unlink/confirm",
headers=HEADERS, json={"otp": "123456"}, timeout=30).raise_for_status()
$conn = json_decode($client->post('/payments/bank-connections/bcn_123/unlink')->getBody(), true);
if (!empty($conn['challenge'])) {
$client->post('/payments/bank-connections/bcn_123/unlink/confirm', ['json' => ['otp' => '123456']]);
}
MB Bank xác nhận hủy liên kết bằng mã, giống khi liên kết. Các ngân hàng khác, và tài khoản đang chờ liên kết, được hủy ngay: phản hồi đã ở revoked và challenge là null. Hủy liên kết tài khoản ACB cũng thu hồi sự đồng ý tại ACB. Để BIDV hoặc Pay2S ngừng đẩy giao dịch, hãy tắt dịch vụ ở phía họ. Hủy liên kết sẽ gửi bank_connection.revoked.
Nhập sao kê
Nhập sao kê giúp khôi phục giao dịch mà ngân hàng không đẩy về, với mọi ngân hàng. Tải sao kê từ ngân hàng, chuyển sang CSV theo mẫu dưới đây, rồi tải lên trên dashboard (Cài đặt thanh toán → Tài khoản ngân hàng → Nhập sao kê ngân hàng) hoặc qua API.
reference,booked_at,direction,amount,description,counterparty_account,counterparty_name
FT1789810452930810,2026-09-19T09:34:12Z,in,99000,DH20250919 chuyen tien,0987654321,NGUYEN VAN KHACH
- cURL
- Node.js
- Python
- PHP
curl https://api.hatang.dev/payments/statements \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg csv "$(cat sao-ke.csv)" \
'{ connection_id: "bcn_123", filename: "sao-ke-thang-9.csv", content: $csv }')"
import { readFile } from "node:fs/promises";
const { data: job } = await hatang.payments.statements.import({
connection_id: "bcn_123",
filename: "sao-ke-thang-9.csv",
content: await readFile("sao-ke.csv", "utf8"),
});
const { data: progress } = await hatang.payments.statements.get(job.id);
with open("sao-ke.csv", encoding="utf-8") as f:
job = requests.post("https://api.hatang.dev/payments/statements", headers=HEADERS, json={
"connection_id": "bcn_123",
"filename": "sao-ke-thang-9.csv",
"content": f.read(),
}, timeout=30).json()
$job = json_decode($client->post('/payments/statements', ['json' => [
'connection_id' => 'bcn_123',
'filename' => 'sao-ke-thang-9.csv',
'content' => file_get_contents('sao-ke.csv'),
]])->getBody(), true);
Lời gọi trả về 202 và các dòng được xử lý ở nền. Theo dõi qua GET /payments/statements/{id}:
{
"object": "statement_import",
"id": "stm_01a0b9042a1c7f0e9d8c7b6a5f4e3d2c",
"connection_id": "bcn_01a0b90428fd7c499145a12c133abe41",
"filename": "sao-ke-thang-9.csv",
"status": "done",
"rows": 1,
"processed_rows": 1,
"error": null,
"created_at": "2026-09-20T02:10:00.000Z",
"completed_at": "2026-09-20T02:10:01.250Z"
}
| Quy tắc | Chi tiết |
|---|---|
| Cột | reference, booked_at, direction, amount, description là bắt buộc; counterparty_account và counterparty_name không bắt buộc. Dòng tiêu đề ghi tên cột, thứ tự tùy ý |
direction | in, out, c, d, credit hoặc debit |
amount | Số nguyên dương, đơn vị VND; chấp nhận dấu phân cách hàng nghìn (99.000, 99,000) |
booked_at | Ngày giờ theo ISO 8601. Giá trị không đọc được sẽ lấy thời điểm nhập |
| Dung lượng | content tối đa 5.000.000 ký tự |
| Trùng lặp | Giao dịch Hạ Tầng đã có (cùng mã tham chiếu và số tiền trên tài khoản) được bỏ qua |
status | queued, processing, done hoặc failed (kèm error) |
Giao dịch nhập từ sao kê đi qua cùng quy trình khớp mã như giao dịch được đẩy về: giao dịch có mã thanh toán sẽ cập nhật payment đó và gửi các sự kiện như bình thường.
Thiếu cột hoặc dòng sai sẽ bị từ chối ngay với 422 validation_error, ví dụ Statement is missing the "amount" column hoặc Statement row 3 is invalid. Khi đó không có gì được lưu.
Lỗi
| Lỗi | Khi nào |
|---|---|
403 environment_mismatch | Ngân hàng thật ở development, hoặc ngân hàng giả lập ở production |
422 missing_required_field | MB Bank thiếu account_number, account_name hoặc holder_id; BIDV hoặc Pay2S thiếu account_number; Pay2S thiếu webhook_token |
422 validation_error | provider không tồn tại, số tài khoản sai định dạng, bank_code với ngân hàng không nhận trường này, hoặc bank_code mà Pay2S không đọc được |
422 bank_link_failed | Ngân hàng từ chối: không có tài khoản, tên hoặc giấy tờ không khớp, tài khoản đã liên kết với doanh nghiệp khác, hoặc chủ tài khoản không đồng ý |
422 invalid_otp | Sai mã |
410 otp_expired | Mã hết hạn; bắt đầu liên kết lại |
409 invalid_state | Tài khoản đã liên kết, đang được liên kết, đã hủy, hoặc không xác nhận bằng mã |
503 provider_unavailable | Ngân hàng không phản hồi; thử lại sau |
Bước tiếp theo
- Nhận giao dịch không cần tạo đơn với
payment_code_pattern. - Tạo thanh toán vào tài khoản đã liên kết.
- Webhook: các sự kiện
bank_connection.*.