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

Webhook

Webhook gửi các sự kiện Thanh toán của toàn doanh nghiệp tới một hoặc nhiều endpoint: mọi payment, mọi giao dịch ngân hàng, mọi thay đổi tài khoản ngân hàng và hóa đơn điện tử, ở cả hai môi trường. Dùng webhook cho giao dịch không qua đơn (transaction.received) và những gì IPN không bao quát. Trang này hướng dẫn tạo endpoint, xác minh lần gửi, và liệt kê mọi sự kiện Thanh toán.

Cách webhook hoạt động trên mọi sản phẩm Hạ Tầng được mô tả ở Webhook.

Tạo endpoint​

Trên dashboard, mở Webhooks và thêm endpoint, hoặc gọi POST /webhooks. Một endpoint nhận cả hai môi trường; mỗi object thanh toán cho biết môi trường qua trường environment.

curl https://api.hatang.dev/webhooks \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"endpoint": "https://shop.example.vn/hatang/webhooks",
"events": ["payment.succeeded", "payment.partially_paid", "payment.overpaid", "transaction.received", "invoice.issued"],
"description": "Backend cửa hàng"
}'

Phản hồi (201) có signing_secret (whsec_…). Secret chỉ hiển thị khi tạo endpoint hoặc khi xoay secret, nên hãy lưu ngay trên server.

Xác minh lần gửi​

Lần gửi theo chuẩn Standard Webhooks / Svix. Mỗi lần gửi có svix-id, svix-timestamp và svix-signature (cùng giá trị đó ở webhook-id, webhook-timestamp và webhook-signature).

import { constructWebhookEvent, HatangSignatureError } from "@hatang/sdk";

app.post("/hatang/webhooks", express.raw({ type: "application/json" }), async (req, res) => {
try {
const event = await constructWebhookEvent(req.body, req.headers, process.env.HATANG_WEBHOOK_SECRET!); // whsec_…
if (await alreadyHandled(req.get("svix-id"))) return res.sendStatus(200);
await queue.add("hatang-webhook", event); // { type, created_at, data }
res.sendStatus(200);
} catch (err) {
if (err instanceof HatangSignatureError) return res.sendStatus(401);
throw err;
}
});

Thư viện svix cũng dùng được: new Webhook(secret).verify(rawBody, headers).

Tự xác minh: base64-decode phần sau whsec_ của secret, tính HMAC-SHA256 của {svix-id}.{svix-timestamp}.{raw body}, base64-encode kết quả, rồi so với từng v1,<chữ ký> trong svix-signature (cách nhau bởi khoảng trắng). Từ chối timestamp lệch quá 5 phút so với hiện tại.

Xoay secret

POST /webhooks/{id}/rotate (hoặc dashboard) tạo secret mới và trả về trong trường secret. Trong 24 giờ, lần gửi được ký bằng cả secret mới và secret cũ, nên svix-signature có hai chữ ký và chữ ký nào cũng xác minh được. Hãy triển khai secret mới trong khoảng thời gian đó.

Cách gửi​

Body{ "type": "...", "created_at": "...", "data": { ... } }
Thành côngBất kỳ mã 2xx nào trong 15 giây. Redirect không được theo
Thử lạiSau 5 giây, 5 phút, 30 phút, 2 giờ, 5 giờ, 10 giờ và 10 giờ: 8 lần
Chống trùngsvix-id giữ nguyên qua mọi lần thử của một lần gửi
Nhật kýGET /webhooks/{id}/deliveries, và Webhooks trên dashboard

Sự kiện Thanh toán​

Payment​

Sự kiệnKhi nàodata
payment.createdTạo paymentPayment
payment.succeededĐã nhận đủ tiềnPayment, kèm transaction
payment.partially_paidNhận thiếuPayment, kèm transaction
payment.overpaidNhận thừaPayment, kèm transaction
payment.expiredQuá expires_at mà chưa có tiềnPayment
payment.canceledBạn đã hủyPayment

Khi payment được ghi nhận thủ công (mark-paid), data có manual: { by, note, amount } thay cho transaction.

Giao dịch và tài khoản ngân hàng​

Sự kiệnKhi nàodata
transaction.receivedCó giao dịch mới trên tài khoản đã liên kết, theo bộ lọc của tài khoảnGiao dịch, kèm merchant_code
bank_connection.activatedLiên kết tài khoản thành côngKết nối ngân hàng
bank_connection.revokedTài khoản bị hủy liên kếtKết nối ngân hàng
bank_connection.needs_reauthNgân hàng yêu cầu liên kết lạiKết nối ngân hàng

Hóa đơn điện tử​

Sự kiệnKhi nào
invoice.draft_created / invoice.draft_updated / invoice.draft_deletedNháp thay đổi
invoice.issuedĐã ký, có số hóa đơn
invoice.tax_accepted / invoice.tax_rejectedPhản hồi của cơ quan thuế
invoice.canceled / invoice.replaced / invoice.adjustedHóa đơn bị hủy, thay thế hoặc điều chỉnh
invoice.failedNhà cung cấp từ chối một bước
invoice.conflictNháp bị sửa ở cả hai phía; giải quyết bằng POST /einvoices/{id}/resolve

data là hóa đơn (object: "einvoice"), kèm payment_id hoặc transaction_id nếu có liên kết. Sự kiện phát sinh trên trang nhà cung cấp có source: "provider".

Ví dụ: payment thanh toán thành công​

{
"type": "payment.succeeded",
"created_at": "2026-09-19T09:34:12.950Z",
"data": {
"object": "payment",
"id": "pay_01a0b904297873e8b3524a26fdca0ab5",
"environment": "production",
"status": "paid",
"amount": 150000,
"paid_amount": 150000,
"code": "HTBF7RDGVQ",
"metadata": { "order_id": "1001" },
"invoice": null,
"paid_at": "2026-09-19T09:34:12.940Z",
"transaction": {
"object": "transaction",
"id": "txn_01a0b90429dc79c7a5689c7a0c485a01",
"amount": 150000,
"description": "CK HTBF7RDGVQ thanh toan",
"reference": "FT1789810452930810",
"payment_id": "pay_01a0b904297873e8b3524a26fdca0ab5",
"match_status": "matched"
}
}
}

Object payment và giao dịch ở đây đã được rút gọn; webhook gửi đầy đủ mọi trường.

Bước tiếp theo​

Xem trang này dạng Markdown