Webhooks — Tổng quan
Webhook là tín hiệu xác thực. Callback trình duyệt (onSuccess)
và các view của dashboard là tiện lợi; webhook là sự thật gốc.
Đảm bảo delivery
- At-least-once. Một event có thể được delivery tối đa 6 lần
nếu server của bạn không trả về 2xx trong timeout. Hãy làm handler
của bạn idempotent — dedup trên
X-Delivery(payload không có trườngevent_id; UUID delivery ổn định là khóa idempotency). - Một event mỗi HTTP request. Không batch.
- Cách ly theo từng endpoint. Nếu bạn có nhiều endpoint đã đăng ký, mỗi cái có track delivery và retry riêng. Một endpoint chậm không làm chậm các endpoint khác.
- Đã ký. Mọi payload mang header
X-Signature(và trong cửa sổ 24 giờ sau khi rotate, cũng cóX-Signature-Prev). Xác thực trước khi làm bất cứ điều gì với body. Xem Xác thực chữ ký.
Các loại event có thể subscribe
| Event | Phát khi… |
|---|---|
payment.settled | Giao dịch chuyển on-chain đã đạt số confirmation của chain. Dùng cái này để đánh dấu order đã thanh toán. |
payment.failed | Một thanh toán fiat bị payment provider từ chối. Không phát cho timeout crypto — những cái đó surface dưới dạng checkout.expired, và thanh toán crypto thiếu surface dưới dạng payment.underpaid. |
payment.underpaid | Vốn đã đến nhưng thiếu so với tổng order (điển hình: phí chuyển stablecoin trừ từ amount). |
payment.overpaid | Vốn đến vượt tổng order. Phần dư được ghi nhận nhưng không auto-refund. |
order.created | Một order mới được mở — hoặc bởi lệnh gọi B2B API của bạn hoặc bởi việc chuyển đổi checkout-session. |
order.canceled | Một order chuyển sang canceled. data.reason của payload phân biệt cancel thủ công với payment_timeout (một order chưa thanh toán đã hết thời gian). |
order.resolved | Một order PARTIAL_PAID được resolve sang PAID — merchant chấp nhận phần thiếu. |
order.reopened | Một order trước đó auto-canceled (canceled_reason=payment_timeout) được merchant mở lại. |
checkout.created | Một người mua đã mở checkout cho một order. |
checkout.completed | Luồng phía người mua đã kết thúc (không có nghĩa là đã settle on-chain — dùng payment.settled cho điều đó). |
checkout.expired | Người mua đã từ bỏ và TTL của session đã hết. |
payment.refund.requested | Một bản ghi refund được tạo — hoặc từ lệnh gọi API do merchant khởi tạo hoặc từ form refund-request do khách hàng submit. |
payment.refund.approved | Một refund pending đã vượt qua workflow approve của bạn. |
payment.refund.rejected | Một refund pending bị từ chối. |
payment.refund.executed | Giao dịch chuyển on-chain của refund đã hoàn tất và bản ghi chuyển sang executed terminal. |
refund_request.created | Một token refund-request đã được phát hành. data.source là b2b / dashboard / renewal. Tùy chọn subscribe — hữu ích cho pipeline audit theo dõi token nào hiện active theo từng order. |
refund_request.renewal_requested | Một người mua bấm “Request new link” sau khi token của họ hết hạn. Khuyến nghị mạnh subscribe — đây là tín hiệu cho merchant rằng widget renewal có item mới để hành động. |
refund_request.renewed | Một renewal được approve và một token mới thay thế cái cũ. data.old_token / data.new_token tạo thành chuỗi audit. |
refund_request.canceled | Một merchant đã chuyển một token sang CANCELED từ dashboard (ví dụ từ chối một renewal request, kill một link đang chạy). Idempotent — chỉ transition đầu tiên phát event. data.reason là ghi chú tùy chọn của merchant. |
Dự kiến (Sắp ra mắt)
Sắp ra mắt. Các event này thuộc về hóa đơn định kỳ và thuê bao, hiện chưa khả dụng. Chúng không nằm trong bảng có thể subscribe ở trên và hiện chưa thể đăng ký. Xem Hóa đơn định kỳ.
| Event dự kiến | Phát khi… |
|---|---|
subscription.created | Một subscription được tạo. |
invoice.created | Một hóa đơn cho chu kỳ thanh toán được tạo. |
invoice.paid | Một hóa đơn được thanh toán. |
subscription.past_due | Một hóa đơn chưa thanh toán sau ngày đến hạn. |
subscription.canceled | Một subscription bị hủy. |
Form endpoint của dashboard liệt kê cùng các event này. Subscribe đến một event không tồn tại sẽ bị reject khi bạn lưu endpoint.
Event test không thể subscribe. Nút Send Test theo từng
endpoint của dashboard gửi một event webhook.test.ping đến đúng một
endpoint đó ngay lập tức, không retry. Nó không nằm trong catalog ở
trên: bạn nhận nó vì bạn có một endpoint đã đăng ký, không phải vì
bạn đã subscribe.
Chỉ subscribe đến các event bạn xử lý. Mỗi endpoint có filter event
riêng; wildcard "*" nghĩa là “mọi event, kể cả các event được
thêm trong tương lai”. Subscribe ít event hơn giữ handler của bạn
đơn giản hơn và có ít retry hơn khi endpoint của bạn gặp lỗi.
Payload + headers
HTTP body chính là object data theo từng event trực tiếp. Không
có envelope ngoài kiểu Stripe — các trường như event type, delivery
ID, và timestamp phát sống trong header thay vào đó. Với
payment.settled body trông như:
{
"receipt_id": "rcp_…",
"order_id": "ord_…",
"payment_intent_id": "pin_…",
"checkout_session_id": "cst_…",
"merchant_id": "mer_…",
"customer_id": "cus_…",
"total": "49.00",
"currency": "USD",
"payment_method": "crypto",
"token": "USDC",
"network": "polygon",
"tx_hash": "0x…",
"deposit_address": "0x…",
"treasury_address": "0x…",
"amount_received": "49.00",
"confirmations": 5,
"metadata": { /* theo từng event */ }
}Các event khác mang bộ trường riêng. Tên trường ổn định
(lower snake_case); tx hash on-chain luôn là tx_hash.
tx_hash là mã định danh giao dịch theo định dạng riêng của network (0x… trên các chain EVM; hash hoặc chữ ký native trên TRON, Solana và TON). deposit_address có thể vắng mặt với TRON, Solana và TON vì người mua trả thẳng vào ví Treasury của bạn, còn confirmations tuân theo Chains & tài sản.
Header trên request inbound
Content-Type: application/json
X-Event: payment.settled
X-Delivery: 7c9e6679-7425-40de-944b-e07fc1f90ae7
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7
X-Timestamp: 1729536000
X-Signature: sha256=9a8b7c…
X-Signature-Prev: sha256=fa31b2… (chỉ trong cửa sổ grace rotate)| Header | Là gì |
|---|---|
X-Event | Loại event (ví dụ payment.settled). Route trên cái này tại lớp proxy nếu bạn muốn bỏ qua parse JSON. |
X-Delivery | UUID nhận diện row delivery. Ổn định qua mọi retry của cùng cặp (event, endpoint) — dùng làm khóa idempotency của bạn. |
Idempotency-Key | Mirror X-Delivery (cùng giá trị). Set trên mọi delivery. |
X-Timestamp | Unix-seconds khi attempt được gửi. Được ký vào payload để một cặp (body, X-Signature) bị capture không thể replay vô hạn — reject delivery có timestamp ngoài cửa sổ tolerance của bạn. |
X-Signature | sha256=<hex> của HMAC-SHA256(secret, X-Timestamp + "." + raw_body). Xem Xác thực chữ ký. |
X-Signature-Prev | Cùng thuật toán với secret trước đó. Chỉ có trong cửa sổ 24 giờ sau khi bạn rotate — cho phép verifier chạy một trong hai key tiếp tục chấp nhận delivery trong cutover. Sau khi cửa sổ đóng, header ngừng được gửi. |
Schedule retry
Nếu endpoint của bạn không trả về 2xx trong timeout, chúng tôi retry
theo schedule này (timestamp tương đối với attempt đầu):
| Attempt | Delay | Tích lũy |
|---|---|---|
| 1 | 0s | 0s |
| 2 | +1 min | 1m |
| 3 | +5 min | 6m |
| 4 | +15 min | 21m |
| 5 | +1 hour | 1h 21m |
| 6 | +6 hours | 7h 21m |
Sau khi attempt 6 fail, delivery được đánh dấu Failed và
email tài khoản của bạn được thông báo. Bạn có thể replay các event
failed từ panel Developers → Webhooks → Delivery history trong
dashboard. Mỗi lần replay là một delivery mới với X-Delivery riêng.
Đăng ký một endpoint
Từ merchant dashboard :
- Developers → Webhooks → + Add endpoint
- Paste URL của bạn — chỉ
https://…(HTTP thuần bị reject; form tạo cũng chặnlocalhost, các dải IP riêng, và URL mang userinfo) - Chọn các event để subscribe (hoặc
*cho tất cả) - Chọn môi trường — test hoặc live (mỗi cái có secret riêng; chúng không bao giờ qua lại)
- Save → dashboard hiển thị secret ký (
whsec_…) một lần. Lưu nó phía server; bạn sẽ cần nó cho hai tính năng tiếp theo.
Bạn có thể đăng ký tối đa 10 endpoint mỗi môi trường mỗi merchant (ví dụ một cho fulfillment production, một cho staging mirror, một cho notifier Slack). Mỗi cái có trạng thái retry và secret riêng.
Hành động vòng đời trên mỗi endpoint
Menu ⋮ trên thẻ mỗi endpoint surface:
- Edit — thay URL, mô tả, hoặc danh sách subscription. URL mới
được re-validate với cùng quy tắc
https:///SSRF như khi tạo. - Send Test — POST đồng bộ một envelope
webhook.test.pingký bằng secret hiện tại của bạn. Dashboard hiển thị HTTP status, latency, và snippet 512 byte của response. Test ping không được retry, nên bạn nhận câu trả lời tức thời. - Rotate Secret — sinh secret mới. Cái trước đó vẫn hợp lệ trong
24 giờ (delivery mang cả
X-SignaturevàX-Signature-Prevtrong cửa sổ để verifier chạy một trong hai key tiếp tục chấp nhận event trong khi bạn redeploy). - Reveal Secret — hiển thị lại secret hiện tại. Gate bởi xác thực 2FA mới và ghi vào audit log; chỉ dùng khi bạn mất copy và Rotate không chấp nhận được.
- Enable / Disable — bật hoặc tắt endpoint mà không mất lịch sử delivery. Endpoint bị disable vẫn ở trong dashboard nhưng không nhận delivery mới.
- Delete — vĩnh viễn. Dùng Disable nếu bạn có thể enable lại sau.
Mẹo cho handler
- Trả 2xx nhanh. Acknowledge với
200 OKtrước khi làm việc nặng — chuyển fulfillment sang background job. Timeout mỗi attempt là 10 giây; giữ response lâu hơn sẽ kích hoạt retry. Timeout ở phía platform và không thể cấu hình theo merchant — liên hệ support nếu handler của bạn thực sự cần thêm thời gian. - Dedup trên
X-Delivery(hoặcIdempotency-Key— cùng giá trị). Kể cả khi bạn trả 2xx, một upstream proxy có thể drop kết nối và kích hoạt retry; delivery ID ổn định qua mọi retry của cùng row delivery, nên đó là key đúng. - Khoan dung với loại event không biết. Event mới có thể xuất hiện; trả 200 và no-op thay vì 4xx, nếu không các delivery đó sẽ cứ retry mãi.
- Log
X-Deliverycạnh business logic của bạn. Khi có gì đó sai, đó là khóa join giữa phía chúng tôi và phía bạn.
Tiếp theo
- Xác thực chữ ký — thuật toán chính xác + pattern chống replay.
- Khái niệm → Sessions — session ở trạng thái nào khi mỗi event được phát.