Nhận giao dịch không cần tạo đơn
Dùng luồng này khi hệ thống của bạn đã có mã đơn và chỉ cần biết khi tiền cho đơn đó về, ví dụ mã QR tĩnh ở quầy thu ngân. Bạn không tạo payment cho từng đơn. Hạ Tầng báo mọi giao dịch vào tài khoản dưới dạng transaction.received, kèm mã đơn của bạn nếu tìm thấy trong nội dung.
Trước khi bắt đầu
- Một kết nối ngân hàng đang hoạt động.
- Một webhook endpoint đăng ký sự kiện
transaction.received. Luồng này không có IPN, vì không có payment nào để gắnipn_url.
1. Khai báo định dạng mã đơn
Đặt payment_code_pattern trên tài khoản ngân hàng: tiền tố và khoảng độ dài. Hạ Tầng sẽ tìm mã này trong nội dung mọi giao dịch.
- 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 '{ "config": { "payment_code_pattern": { "prefix": "DH", "min_length": 4, "max_length": 8, "charset": "digits" } } }'
await hatang.payments.bankConnections.update("bcn_123", {
config: { payment_code_pattern: { prefix: "DH", min_length: 4, max_length: 8, charset: "digits" } },
});
requests.patch("https://api.hatang.dev/payments/bank-connections/bcn_123", headers=HEADERS, json={
"config": {"payment_code_pattern": {"prefix": "DH", "min_length": 4, "max_length": 8, "charset": "digits"}},
}, timeout=10).raise_for_status()
$client->patch('/payments/bank-connections/bcn_123', ['json' => [
'config' => ['payment_code_pattern' => ['prefix' => 'DH', 'min_length' => 4, 'max_length' => 8, 'charset' => 'digits']],
]]);
Cách tìm mã
- Nội dung được chuẩn hóa: bỏ dấu,
đthànhD, viết hoa toàn bộ, và bỏ mọi ký tự không phải chữ hoặc số.ck dh 2025-0919 cam onthànhCKDH20250919CAMON. - Hạ Tầng tìm tiền tố theo sau là
min_lengthđếnmax_lengthký tự thuộccharset, và lấy kết quả đầu tiên, dài nhất có thể.
Với cấu hình trên, DH20250919 được tìm thấy trong ck dh 2025-0919 cam on.
Khoảng trắng bị bỏ trước khi tìm, nên với charset: "alnum" các chữ phía sau sẽ dính vào mã: DH1001 cam on thành DH1001CAMON và kết quả là DH1001CAMO. Nên dùng digits, hoặc cho mọi mã dài đúng max_length.
2. Hiển thị VietQR chứa mã
GET /payments/qr tạo VietQR cho số tiền và nội dung bất kỳ. Không lưu gì.
- cURL
- Node.js
- Python
- PHP
# JSON kèm chuỗi EMVCo
curl "https://api.hatang.dev/payments/qr?amount=99000&content=DH20250919" \
-H "Authorization: Bearer $HATANG_API_KEY"
# Ảnh SVG
curl "https://api.hatang.dev/payments/qr?amount=99000&content=DH20250919&format=svg" \
-H "Authorization: Bearer $HATANG_API_KEY" -o qr.svg
const { data: qr } = await hatang.payments.qr({ amount: 99_000, content: "DH20250919" });
// Render qr.payload bằng thư viện QR bất kỳ.
qr = requests.get("https://api.hatang.dev/payments/qr", headers=HEADERS,
params={"amount": 99000, "content": "DH20250919"}, timeout=10).json()
$qr = json_decode($client->get('/payments/qr', ['query' => ['amount' => 99000, 'content' => 'DH20250919']])->getBody(), true);
{
"object": "vietqr",
"payload": "00020101021238530010A0000007270123000697042201099104526580208QRIBFTTA53037045405990005802VN62140810DH202509196304FCC9",
"connection_id": "bcn_01a0b90428fd7c499145a12c133abe41",
"bank": { "bin": "970422", "account_number": "910452658", "account_name": "NGUYEN VAN A" },
"amount": 99000,
"content": "DH20250919"
}
| Tham số | Ý nghĩa |
|---|---|
amount | VND. Bỏ trống để người mua tự nhập số tiền (QR dán ở quầy) |
content | Nội dung điền sẵn, tối đa 50 ký tự |
connection_id | Tài khoản nào; chỉ cần khi có nhiều tài khoản |
format | json (mặc định) hoặc svg |
logo | Với format=svg: 0 để bỏ logo Hạ Tầng khỏi ảnh |
QR chỉ điền sẵn nội dung. Người chuyển khoản thủ công phải tự gõ mã, vì vậy hãy in mã cạnh QR.
3. Nhận transaction.received
{
"type": "transaction.received",
"created_at": "2026-09-19T09:34:12.946Z",
"data": {
"object": "transaction",
"id": "txn_01a0b90429dc79c7a5689c7a0c485a01",
"environment": "production",
"connection_id": "bcn_01a0b90428fd7c499145a12c133abe41",
"direction": "in",
"amount": 99000,
"currency": "VND",
"description": "DH20250919 chuyen tien",
"code": "DH20250919",
"merchant_code": "DH20250919",
"reference": "FT1789810452930810",
"counterparty": { "account": "0987654321", "name": "NGUYEN VAN KHACH", "bank": "970436" },
"booked_at": "2026-09-19T09:34:12.930Z",
"source": "push",
"payment_id": null,
"match_status": "unmatched",
"invoice_exempt": null,
"created_at": "2026-09-19T09:34:12.946Z"
}
}
| Trường | Ý nghĩa |
|---|---|
merchant_code | Mã đơn của bạn tìm được trong nội dung; null nếu không tìm thấy |
code | Mã lưu trên giao dịch: mã thanh toán Hạ Tầng khi giao dịch trả cho một payment, ngược lại là mã đơn của bạn |
reference | Mã tham chiếu của ngân hàng |
source | Hạ Tầng biết giao dịch bằng cách nào: push (ngân hàng báo), reconcile (đọc từ lịch sử tài khoản), import (sao kê) hoặc simulator (development) |
payment_id, match_status | Có giá trị khi giao dịch trả cho một payment của bạn (matched); ngược lại là unmatched |
counterparty.bank | Mã BIN ngân hàng người chuyển, nếu ngân hàng cung cấp |
Handler của bạn nên:
- Xác minh chữ ký webhook (xem Webhook).
- Tìm đơn theo
merchant_code, và soamountvới số tiền cần thu. - Lưu
data.id. Cùng một giao dịch không bao giờ được báo hai lần, nhưng endpoint có thể nhận lại cùng một lần gửi sau khi timeout. - Kiểm tra
environmentvàdirection.
Tình huống đặc biệt
| Tình huống | Bạn nhận được |
|---|---|
| Người mua quên ghi mã | merchant_code: null. Đối chiếu theo số tiền và counterparty.name, hoặc hỏi người mua |
| Người mua chuyển sai số tiền | Số tiền đúng như đã chuyển; bạn tự quyết định |
| Người mua chuyển hai lần | Hai giao dịch với id và reference khác nhau |
| Ngân hàng đẩy cùng giao dịch hai lần | Một giao dịch (cùng mã tham chiếu và số tiền trên tài khoản) |
| Giao dịch chứa cả mã thanh toán | Giao dịch cập nhật payment đó (payment.succeeded…) và transaction.received có payment_id. Một giao dịch không bao giờ bị tính hai lần |
Lọc giao dịch được báo
Theo từng tài khoản ngân hàng, trong transaction_events của config hoặc trên dashboard:
| Cấu hình | Mặc định | Tác dụng |
|---|---|---|
direction | in | all báo cả tiền ra |
require_code | false | true chỉ báo giao dịch tìm thấy mã hoặc khớp với payment |
min_amount | 0 | Giao dịch dưới số tiền này không được báo |
Danh sách và tra cứu giao dịch
Mọi giao dịch đều được lưu, dù có được báo hay không.
- cURL
- Node.js
- Python
- PHP
curl "https://api.hatang.dev/payments/transactions?direction=in&match_status=unmatched&limit=50" \
-H "Authorization: Bearer $HATANG_API_KEY"
const { data: page } = await hatang.payments.transactions.list({ direction: "in", match_status: "unmatched", limit: 50 });
if (page.has_more) await hatang.payments.transactions.list({ after: page.next_cursor! });
page = requests.get("https://api.hatang.dev/payments/transactions", headers=HEADERS,
params={"direction": "in", "match_status": "unmatched", "limit": 50}, timeout=10).json()
$page = json_decode($client->get('/payments/transactions', ['query' => [
'direction' => 'in', 'match_status' => 'unmatched', 'limit' => 50,
]])->getBody(), true);
| Tham số | Ý nghĩa |
|---|---|
limit | 1 đến 100, mặc định 20 |
after | next_cursor của trang trước |
connection_id | Một tài khoản ngân hàng |
direction | in hoặc out |
match_status | matched, unmatched hoặc ignored |
payment_id | Các giao dịch đã trả cho payment này |
Phản hồi có dạng { "object": "list", "data": [...], "has_more": true, "next_cursor": "txn_..." }, mới nhất trước. GET /payments/transactions/{id} trả về một giao dịch.
Bước tiếp theo
- Phát hành hóa đơn điện tử cho một giao dịch bằng
transaction_id. - Nhập sao kê để khôi phục giao dịch ngân hàng bỏ sót.
- Tạo thanh toán khi bạn cần trang checkout và hạn thanh toán.