Nhận kết quả
Hạ Tầng báo cho server của bạn về payment theo ba cách: lời gọi IPN tới ipn_url của từng payment, webhook cho mọi thứ trong doanh nghiệp, và polling API bất cứ lúc nào. Trang này so sánh ba cách, cho thấy chính xác một lời gọi IPN trông thế nào, và liệt kê những gì handler của bạn phải kiểm tra.
Chọn kênh
| IPN | Webhook | Polling | |
|---|---|---|---|
| Phạm vi | Một payment (ipn_url khai báo khi tạo) | Mọi thứ trong doanh nghiệp, cả hai môi trường | Bất kỳ thứ gì, bất cứ lúc nào |
| Gửi khi | Mọi thay đổi payment.* trừ created, và mọi thay đổi invoice.* của payment đó | Các loại sự kiện endpoint đăng ký | – |
| Body | { id, event_id, type, environment, created_at, data: <payment> } | { type, created_at, data } | Object |
| Chữ ký | Hatang-Signature: HMAC-SHA256 của body với secret key | Svix: svix-id, svix-timestamp, svix-signature | API key |
| Thử lại | 1 phút, 5 phút, 15 phút, 1 giờ, 3 giờ, 6 giờ, 12 giờ (8 lần) | 5 giây, 5 phút, 30 phút, 2 giờ, 5 giờ, 10 giờ, 10 giờ (8 lần) | – |
| Timeout | 10 giây | 15 giây | – |
| Nhật ký gửi | GET /payments/{id}/ipn-deliveries | GET /webhooks/{id}/deliveries | – |
Phần lớn cửa hàng dùng IPN cho đơn hàng, vì IPN mang theo toàn bộ payment và chỉ cần khai báo ipn_url. Dùng webhook cho giao dịch không qua đơn (transaction.received), sự kiện tài khoản ngân hàng, và mọi thứ bạn muốn nhận cho cả doanh nghiệp. Polling là phương án dự phòng.
Yêu cầu với endpoint
- URL IPN và webhook phải là https trên internet công khai. Các địa chỉ như
localhost,10.x,192.168.xhay169.254.169.254bị từ chối, cả khi lưu URL lẫn khi Hạ Tầng kết nối. - Lời gọi được tính là đã gửi khi endpoint trả 2xx. Redirect không được theo, nên
301hay302đều tính là lỗi. - Trả lời nhanh và xử lý sau (qua queue). Trả lời chậm bị tính là lỗi và lời gọi sẽ được gửi lại.
Handler của bạn phải
- Xác minh chữ ký trên body gốc, trước khi parse JSON (IPN, webhook).
- Chống trùng.
id(vàevent_id) của IPN vàsvix-idcủa webhook giữ nguyên qua mọi lần thử lại. - Tin trạng thái, không tin thứ tự đến. Lời gọi có thể đến sai thứ tự hoặc hai lần. Đọc
data.status(hoặc gọiGET /payments/{id}), và chỉ đưa đơn hàng tiến lên. - Kiểm tra số tiền và môi trường. So
data.paid_amountvới đơn, và kiểm traenvironment: payment development không bao giờ được hoàn tất đơn thật.
// Chỉ đưa đơn tiến lên; bỏ qua mọi lời gọi khiến đơn lùi lại.
const RANK = { pending: 0, expired: 1, partially_paid: 2, paid: 3, overpaid: 3, canceled: 0 } as const;
async function applyIpn(ipn: IpnEvent) {
if (ipn.environment !== process.env.EXPECTED_ENVIRONMENT) return;
const order = await db.orders.find(ipn.data.metadata.order_id);
if (!order || RANK[ipn.data.status] <= RANK[order.paymentStatus]) return;
if ((ipn.data.status === "paid" || ipn.data.status === "overpaid") && ipn.data.paid_amount >= order.total) {
await db.orders.markPaid(order.id, ipn.data.id);
} else {
await db.orders.setPaymentStatus(order.id, ipn.data.status);
}
}
Một lời gọi IPN
Đây là những gì endpoint của bạn nhận được:
POST /hatang/ipn HTTP/1.1
Content-Type: application/json
User-Agent: Hatang-IPN/1.0
Hatang-Access-Key: ak_Example0AccessKey0000000
Hatang-Signature: 8f0c3b1e5d7a9c2f4e6b8a0d1c3e5f7a9b2d4f6e8a0c2e4f6b8d0a2c4e6f8b0d
Hatang-Signature-Algorithm: HMAC-SHA256
Hatang-Event: payment.succeeded
Hatang-Delivery-Id: ipn_01a0b90429d87cf89e37fc2f66040ec5
Hatang-Environment: production
{"id":"ipn_01a0b90429d87cf89e37fc2f66040ec5","event_id":"de_01a0b90429b57b41a2ef3510d6bf8036","type":"payment.succeeded","environment":"production","created_at":"2026-09-19T09:34:12.952Z","data":{"object":"payment","id":"pay_01a0b904297873e8b3524a26fdca0ab5","status":"paid","amount":150000,"paid_amount":150000,"code":"HTBF7RDGVQ","metadata":{"order_id":"1001"},"invoice":null, …}}
| Header | Ý nghĩa |
|---|---|
Hatang-Access-Key | Lời gọi thuộc tài khoản và môi trường nào (ak_…, hoặc ak_test_… ở development) |
Hatang-Signature | HMAC-SHA256 dạng hex thường của body gốc |
Hatang-Signature-Algorithm | Luôn là HMAC-SHA256 |
Hatang-Event | Loại sự kiện, giống type trong body |
Hatang-Delivery-Id | Giống id trong body; dùng để chống trùng |
Hatang-Environment | development hoặc production |
| Trường trong body | Ý nghĩa |
|---|---|
id | Mã lần gửi (ipn_…), giữ nguyên qua mọi lần thử lại |
event_id | Sự kiện gây ra lời gọi |
type | payment.succeeded, payment.partially_paid, payment.overpaid, payment.expired, payment.canceled, hoặc một sự kiện invoice.* của payment này |
environment | development hoặc production |
created_at | Thời điểm tạo lời gọi |
data | Toàn bộ object payment, gồm cả tóm tắt invoice |
data là payment tại thời điểm tạo lời gọi. Với sự kiện invoice.*, data vẫn là payment, với trạng thái mới nằm trong data.invoice.
Thử lại và nhật ký gửi
Lời gọi không nhận được 2xx trong 10 giây sẽ được thử lại sau 1 phút, 5 phút, 15 phút, 1 giờ, 3 giờ, 6 giờ và 12 giờ: 8 lần trong khoảng một ngày. Sau lần cuối, lần gửi chuyển sang exhausted. Các lần thử lại được ký bằng secret key hiện hành tại thời điểm gửi.
GET /payments/{id}/ipn-deliveries liệt kê 100 lời gọi gần nhất của một payment, mới nhất trước, kèm status (pending, success, failed, exhausted), attempt, response_status, response_ms, last_error và next_attempt_at. Trang payment trên dashboard hiển thị cùng danh sách. Nếu endpoint của bạn bị lỗi lâu hơn thời gian thử lại, hãy polling các payment bị ảnh hưởng.
Polling API
Đọc payment khi bạn cần trạng thái hiện tại, ví dụ khi người mua quay lại return_url, hoặc trong job định kỳ rà soát đơn chưa thanh toán.
- cURL
- Node.js
- Python
- PHP
curl https://api.hatang.dev/payments/pay_123 \
-H "Authorization: Bearer $HATANG_API_KEY"
const { data: check } = await hatang.payments.verify("pay_123", { order_id: "1001", amount: 150_000, environment: "production" });
if (check?.verified) await fulfil("1001");
p = hatang.payments.get("pay_123")
if p["status"] in ("paid", "overpaid") and p["paid_amount"] >= 150000 and p["environment"] == "production":
fulfil("1001")
$p = json_decode($client->get('/payments/pay_123')->getBody(), true);
if (in_array($p['status'], ['paid', 'overpaid'], true) && $p['paid_amount'] >= 150000 && $p['environment'] === 'production') {
fulfil('1001');
}
Để rà soát nhiều payment, dùng GET /payments?status=paid,overpaid và phân trang bằng after. Polling được tính vào giới hạn 20 request Thanh toán mỗi giây.
Bước tiếp theo
- Xác minh IPN bằng Node.js, PHP, Python và C#.
- Webhook: tạo endpoint và danh sách sự kiện.
- Lên production.