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

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​

IPNWebhookPolling
Phạm viMột payment (ipn_url khai báo khi tạo)Mọi thứ trong doanh nghiệp, cả hai môi trườngBất kỳ thứ gì, bất cứ lúc nào
Gửi khiMọ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 keySvix: svix-id, svix-timestamp, svix-signatureAPI key
Thử lại1 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)–
Timeout10 giây15 giây–
Nhật ký gửiGET /payments/{id}/ipn-deliveriesGET /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.x hay 169.254.169.254 bị 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 301 hay 302 đề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​

  1. Xác minh chữ ký trên body gốc, trước khi parse JSON (IPN, webhook).
  2. Chống trùng. id (và event_id) của IPN và svix-id của webhook giữ nguyên qua mọi lần thử lại.
  3. 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ọi GET /payments/{id}), và chỉ đưa đơn hàng tiến lên.
  4. Kiểm tra số tiền và môi trường. So data.paid_amount với đơn, và kiểm tra environment: 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-KeyLời gọi thuộc tài khoản và môi trường nào (ak_…, hoặc ak_test_… ở development)
Hatang-SignatureHMAC-SHA256 dạng hex thường của body gốc
Hatang-Signature-AlgorithmLuôn là HMAC-SHA256
Hatang-EventLoại sự kiện, giống type trong body
Hatang-Delivery-IdGiống id trong body; dùng để chống trùng
Hatang-Environmentdevelopment hoặc production
Trường trong bodyÝ nghĩa
idMã lần gửi (ipn_…), giữ nguyên qua mọi lần thử lại
event_idSự kiện gây ra lời gọi
typepayment.succeeded, payment.partially_paid, payment.overpaid, payment.expired, payment.canceled, hoặc một sự kiện invoice.* của payment này
environmentdevelopment hoặc production
created_atThời điểm tạo lời gọi
dataToà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 https://api.hatang.dev/payments/pay_123 \
-H "Authorization: Bearer $HATANG_API_KEY"

Để 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​

Xem trang này dạng Markdown