Chuyển tới nội dung chính

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ận 403 environment_mismatch.

Endpoint​

MethodEndpointChức năng
GET/payments/bank-connectionsDanh sách tài khoản đã liên kết (bỏ qua tài khoản đã hủy)
POST/payments/bank-connectionsBắ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}/confirmXác nhận liên kết bằng mã của ngân hàng
POST/payments/bank-connections/{id}/unlinkBắt đầu hủy liên kết
POST/payments/bank-connections/{id}/unlink/confirmXác nhận hủy liên kết bằng mã của ngân hàng
POST/payments/statementsNhậ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àngproviderlink_modeBạn cần làm
MB BankmbbankotpGử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ã đó
BIDVbidvbank_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
ACBacbredirectGử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
Pay2Spay2sbank_registeredLiê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ậpsimulatorotpXác nhận bằng mã 000000

1. Bắt đầu liên kết​

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"
}'
TrườngMô tả
providerBắt buộc. mbbank, bidv, acb hoặc pay2s ở production; simulator ở development
account_numberBắ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_nameTên chủ tài khoản đúng như ngân hàng ghi nhận. Bắt buộc với MB Bank
account_typepersonal (mặc định) hoặc business
holder_idCCCD (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_codeVớ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_tokenChỉ dùng và bắt buộc với Pay2S: token của Hook bạn tạo trên Pay2S
labelTên bạn đặt cho tài khoản, tối đa 100 ký tự
configCấ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 https://api.hatang.dev/payments/bank-connections/bcn_123/confirm \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "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 POST với provider: "bidv" và account_number. Kết nối ở trạng thái pending_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ọi confirm sẽ nhận 409 invalid_state.
  • ACB. POST với provider: "acb" và account_number, rồi đưa chủ tài khoản tới challenge.authorize_url trong 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 sang active. 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ường consent củ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_url từ 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 đó POST với provider: "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ại VCB, ICB, TCB, BIDV, ACB, MB và 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_reauthNgân hàng yêu cầu liên kết lại
revokedĐã hủy liên kết
errorNgâ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 -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 }
}
}'
TrườngMặc địnhMô tả
code_prefixHTTiền tố của mã thanh toán tự sinh, 2 đến 5 chữ in hoa (luồng đơn hàng)
payment_code_patternnullĐị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_length3 / 10Độ dài phần sau tiền tố, từ 1 đến 30
payment_code_pattern.charsetdigitsdigits hoặc alnum (chữ và số)
transaction_events.directioninin, hoặc all để báo cả tiền ra
transaction_events.require_codefalseChỉ báo giao dịch tìm thấy mã hoặc khớp với một payment
transaction_events.min_amount0Khô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 -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" }'

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 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 }')"

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ắcChi tiết
Cộtreference, 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 ý
directionin, out, c, d, credit hoặc debit
amountSố 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_atNgày giờ theo ISO 8601. Giá trị không đọc được sẽ lấy thời điểm nhập
Dung lượngcontent tối đa 5.000.000 ký tự
Trùng lặpGiao 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
statusqueued, 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ỗiKhi nào
403 environment_mismatchNgân hàng thật ở development, hoặc ngân hàng giả lập ở production
422 missing_required_fieldMB 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_errorprovider 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_failedNgâ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_otpSai mã
410 otp_expiredMã hết hạn; bắt đầu liên kết lại
409 invalid_stateTà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_unavailableNgân hàng không phản hồi; thử lại sau

Bước tiếp theo​

Xem trang này dạng Markdown