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

Tạo thanh toán

Payment là yêu cầu thu tiền cho một đơn hàng. Mỗi payment có mã chuyển khoản, VietQR và trang checkout riêng, và Hạ Tầng chuyển nó sang paid khi có giao dịch mang mã đó. Trang này hướng dẫn tạo payment, các trạng thái, mã thanh toán, và các lời gọi hủy hoặc ghi nhận thanh toán thủ công.

Trước khi bắt đầu​

  • Một kết nối ngân hàng đang hoạt động ở môi trường bạn dùng.
  • API key có payments:write.
  • Một endpoint HTTPS cho IPN, nếu bạn muốn nhận lời gọi cho payment này.

1. Tạo payment​

Tạo khi khách bấm thanh toán. Gửi Idempotency-Key (mã đơn là lựa chọn tốt) để request thử lại trả về cùng một payment thay vì tạo cái thứ hai.

curl https://api.hatang.dev/payments \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Idempotency-Key: order-1001" \
-H "Content-Type: application/json" \
-d '{
"amount": 150000,
"description": "Đơn hàng #1001",
"expires_in": 3600,
"return_url": "https://shop.example.vn/orders/1001",
"ipn_url": "https://shop.example.vn/hatang/ipn",
"customer": { "name": "Trần Thị B", "email": "[email protected]" },
"metadata": { "order_id": "1001" }
}'
TrườngÝ nghĩa
amountBắt buộc. VND, đã gồm VAT: số tiền người mua chuyển. Số nguyên từ 1 đến 100.000.000.000
descriptionHiển thị cho người mua trên trang checkout, tối đa 200 ký tự. Đây không phải nội dung chuyển khoản
codeMã chuyển khoản của bạn thay cho mã tự sinh; xem Mã của riêng bạn
connection_idTài khoản ngân hàng nhận tiền. Chỉ cần khi có nhiều tài khoản đang hoạt động
expires_inSố giây tới khi hết hạn: 1 đến 2.592.000 (30 ngày). Mặc định 86.400 (24 giờ)
return_urlTrang checkout chuyển người mua về đây sau khi trả
ipn_urlEndpoint nhận IPN của payment này (xem Nhận kết quả). Chỉ chấp nhận https công khai
customername, email, phone. Của bạn; trả lại nguyên văn
metadataDữ liệu key-value của bạn. Key tối đa 40 ký tự; giá trị là chuỗi (tối đa 500 ký tự), số, boolean hoặc null. Trả lại nguyên văn
invoicePhát hành hóa đơn điện tử khi đã trả: yêu cầu xuất hóa đơn (người mua, dòng hàng, VAT). Xem Hóa đơn điện tử

Idempotency​

Cùng Idempotency-Key với cùng body sẽ trả về payment tạo lần đầu, mã 201. Cùng key nhưng body khác sẽ trả về 400 invalid_idempotent_request. Key dài tối đa 256 ký tự và có phạm vi theo doanh nghiệp và môi trường.

Phản hồi​

{
"object": "payment",
"id": "pay_01a0b904297873e8b3524a26fdca0ab5",
"environment": "production",
"status": "pending",
"amount": 150000,
"paid_amount": 0,
"currency": "VND",
"code": "HTBF7RDGVQ",
"description": "Đơn hàng #1001",
"connection_id": "bcn_01a0b90428fd7c499145a12c133abe41",
"bank": { "bin": "970422", "name": "MB Bank", "account_number": "910452658", "account_name": "NGUYEN VAN A" },
"qr": {
"payload": "00020101021238530010A0000007270123000697042201099104526580208QRIBFTTA530370454061500005802VN62140810HTBF7RDGVQ63044DEC",
"image_url": "https://api.hatang.dev/pay/pay_01a0b904297873e8b3524a26fdca0ab5/qr.svg"
},
"checkout_url": "https://app.hatang.dev/pay/pay_01a0b904297873e8b3524a26fdca0ab5",
"return_url": "https://shop.example.vn/orders/1001",
"ipn_url": "https://shop.example.vn/hatang/ipn",
"customer": { "name": "Trần Thị B", "email": "[email protected]" },
"metadata": { "order_id": "1001" },
"invoice": null,
"expires_at": "2026-09-19T10:34:12.856Z",
"paid_at": null,
"canceled_at": null,
"created_at": "2026-09-19T09:34:12.856Z"
}

Lưu id cùng đơn hàng của bạn. Webhook payment.created được gửi (sự kiện này không có IPN).

Lỗi​

LỗiKhi nàoCách xử lý
422 validation_errorThiếu hoặc sai trường; thông báo nêu tên trường (amount: Number must be greater than or equal to 1)Sửa request
422 validation_error ở ipn_urlURL trỏ tới địa chỉ nội bộ, hoặc không phải httpsDùng URL HTTPS công khai
422 validation_error "Several bank accounts are linked; pass connection_id"Có nhiều hơn một tài khoản đang hoạt độngGửi connection_id
422 no_bank_connectionKhông có tài khoản đang hoạt động, hoặc connection_id không hoạt độngLiên kết hoặc liên kết lại tài khoản
409 code_takencode của bạn đã được một payment khác vào tài khoản này dùngDùng mã khác
400 invalid_idempotent_requestKey đã dùng với body khácDùng key mới cho đơn mới
429 rate_limit_exceededHơn 20 request Thanh toán mỗi giây cho doanh nghiệp của bạnThử lại sau thời gian trong header Retry-After

2. Để người mua thanh toán​

Chuyển tới checkout_url, hoặc tự hiển thị qr và thông tin bank. Xem Cách người mua thanh toán.

3. Nhận kết quả​

Chờ lời gọi IPN hoặc webhook (xem Nhận kết quả), rồi giao hàng khi status là paid hoặc overpaid và paid_amount đủ cho đơn. Không bao giờ giao hàng dựa trên trình duyệt.

Trạng thái​

statusÝ nghĩaSự kiện
pendingĐang chờ chuyển khoảnpayment.created
paidĐã nhận đủ; paid_at được đặtpayment.succeeded
partially_paidNhận ít hơn số tiền. Giao dịch tiếp theo cùng mã sẽ cộng dồnpayment.partially_paid
overpaidNhận nhiều hơn số tiềnpayment.overpaid
expiredKhông có tiền nào trước expires_atpayment.expired
canceledBạn đã hủy khi payment còn pendingpayment.canceled

payment.succeeded, payment.partially_paid và payment.overpaid kèm theo transaction ngân hàng tương ứng.

Tình huống đặc biệt​

  • Tiền về muộn. Giao dịch đến sau expires_at vẫn được ghi nhận: payment chuyển từ expired sang paid (hoặc partially_paid, overpaid) và bạn được báo. Tự quyết định có giao đơn trễ hay không.
  • Payment trả thiếu không hết hạn. Chỉ payment pending mới chuyển sang expired. Payment partially_paid giữ nguyên trạng thái cho tới khi phần còn lại về hoặc bạn ghi nhận thủ công.
  • Trả hai lần. Giao dịch thứ hai mang mã của payment đã paid sẽ chuyển nó sang overpaid, và paid_amount là tổng. Hoàn tiền thực hiện ngoài Hạ Tầng.
  • Payment đã hủy không còn được khớp. Giao dịch mang mã của nó đến dưới dạng transaction.received chưa khớp.
  • Thời điểm hết hạn. Việc xử lý hết hạn chạy mỗi 30 giây, nên payment.expired đến ngay sau expires_at.

Mã thanh toán​

code gồm tiền tố (HT mặc định, đổi được theo tài khoản ngân hàng qua config.code_prefix) và 8 ký tự không dễ nhầm: không có 0, O, 1, I. Người mua phải ghi mã vào nội dung chuyển khoản. Xung quanh có gì cũng được (CK HTBF7RDGVQ thanh toan); chữ hoa thường, khoảng trắng, dấu câu và dấu tiếng Việt đều được bỏ qua. VietQR đã chứa sẵn mã, nên người quét QR không phải gõ.

Mã của riêng bạn​

Gửi code và mã đó sẽ là nội dung chuyển khoản thay cho mã tự sinh: ví dụ số đơn hàng của bạn, để sao kê ngân hàng và đơn hàng khớp nhau.

{ "amount": 275000, "code": "DH1002" }
  • Chỉ gồm chữ và số, từ 6 đến 20 ký tự; được viết hoa.
  • Mã phải chưa được dùng trên tài khoản ngân hàng đó, nếu không lời gọi trả về 409 code_taken. Thử lại với cùng Idempotency-Key sẽ trả về payment đã có.
  • Mã dài an toàn hơn. Mã được tìm bằng cách dò trong nội dung chuyển khoản, nên một mã ngắn, phổ biến (ORDER1) có thể khớp nhầm giao dịch tình cờ chứa nó. Mã có tiền tố riêng của bạn (DH1002) là đủ đặc trưng.
  • Khi một mã nằm trong mã khác (DH1002 và DH10024), mã dài hơn được nhận giao dịch chứa nó.

Đọc và liệt kê payment​

curl https://api.hatang.dev/payments/pay_123 -H "Authorization: Bearer $HATANG_API_KEY"
curl "https://api.hatang.dev/payments?status=paid,overpaid&limit=50" -H "Authorization: Bearer $HATANG_API_KEY"
Tham sốÝ nghĩa
limit1 đến 100, mặc định 20
afternext_cursor của trang trước
statusMột hoặc nhiều trạng thái, cách nhau bởi dấu phẩy
connection_idMột tài khoản ngân hàng

Danh sách trả về { "object": "list", "data": [...], "has_more": false, "next_cursor": null }, mới nhất trước.

Hủy payment​

curl -X POST https://api.hatang.dev/payments/pay_123/cancel \
-H "Authorization: Bearer $HATANG_API_KEY"

Chỉ hủy được payment pending. Trạng thái khác trả về 409 invalid_state ("A paid payment cannot be canceled").

Ghi nhận tiền nhận bằng cách khác​

Khi tiền đến ngoài ngân hàng (tiền mặt, hoặc chuyển khoản không có mã), hãy ghi nhận thủ công. Dashboard có thao tác tương tự: Đánh dấu đã thanh toán trên trang payment.

curl https://api.hatang.dev/payments/pay_123/mark-paid \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "note": "Khách trả tiền mặt tại quầy" }'
TrườngÝ nghĩa
amountSố tiền đã nhận. Mặc định là phần còn thiếu (amount - paid_amount)
noteTối đa 500 ký tự, được lưu trong sự kiện

Áp dụng cho payment pending, partially_paid và expired (trạng thái khác trả về 409 invalid_state). Payment chuyển sang paid, partially_paid hoặc overpaid như khi có giao dịch thật, và data của sự kiện webhook có thêm manual: { by, note, amount } để bạn phân biệt.

Bước tiếp theo​

Xem trang này dạng Markdown