Skip to Content
WebhooksTổng quan

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ường event_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 + retry riêng. Một URL merchant chậm không thể bỏ đói các URL khác — mỗi host có circuit breaker riêng.
  • Đã 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

EventPhát khi…
payment.settledGiao 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.failedMột thanh toán fiat bị provider từ chối tường minh (hiện tại: webhook Stripe báo hiệu failure). 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.underpaidVốn đã đến nhưng thiếu so với tổng order (điển hình: phí chuyển stablecoin trừ từ amount).
payment.overpaidVốn đến vượt tổng order. Phần dư được ghi nhận nhưng không auto-refund.
order.createdMộ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.canceledMột order chuyển sang canceled. data.reason của payload phân biệt cancel thủ công với payment_timeout (order chưa thanh toán cũ bị worker sweep).
order.resolvedMột order PARTIAL_PAID được resolve sang PAID — merchant chấp nhận phần thiếu.
order.reopenedMột order trước đó auto-canceled (canceled_reason=payment_timeout) được merchant mở lại.
checkout.createdMột người mua đã mở checkout cho một order.
checkout.completedLuồ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.expiredNgười mua đã từ bỏ và TTL của session đã hết.
payment.refund.requestedMộ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.approvedMột refund pending đã vượt qua workflow approve của bạn.
payment.refund.rejectedMột refund pending bị từ chối.
payment.refund.executedGiao dịch chuyển on-chain của refund đã hoàn tất và bản ghi chuyển sang executed terminal.
refund_request.createdMột token refund-request đã được phát hành. data.sourceb2b / 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_requestedMộ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.renewedMộ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.canceledMộ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.

Dashboard fetch danh sách này từ GET /v1/webhooks/event-types để form tạo / chỉnh sửa endpoint luôn khớp với những gì platform thực sự phát. Subscribe đến một event chúng tôi không ship sẽ bị reject tại thời điểm tạo với lỗi rõ ràng.

Event test không thể subscribe. Nút Send Test theo từng endpoint của dashboard POST một envelope webhook.test.ping đồng bộ đến đúng một endpoint đó (bỏ qua pipeline retry), và path “Send test event” cấp merchant legacy fan-out một envelope webhook.test đến mọi endpoint active bất kể filter của nó. Cả hai đều không xuất hiện trong catalog ở trên — bạn nhận chúng nhờ có một endpoint đã đăng ký, không phải bằng cách 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 sạch hơn giảm surface mà chúng tôi phải retry khi có 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 — xem các publisher struct trong payment-service/internal/domain/events.go cho hình dáng canonical cho đến khi tài liệu theo từng event đáp đất. Tên trường ổn định (lower snake_case); tx hash on-chain luôn là tx_hash (không phải transaction_hash).

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)
HeaderLà gì
X-EventLoạ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-DeliveryUUID 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-KeyMirror X-Delivery (cùng giá trị). Set trên mọi delivery — theo convention của Stripe / GitHub.
X-TimestampUnix-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-Signaturesha256=<hex> của HMAC-SHA256(secret, X-Timestamp + "." + raw_body). Xem Xác thực chữ ký.
X-Signature-PrevCù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):

AttemptDelayTích lũy
10s0s
2+1 min1m
3+5 min6m
4+15 min21m
5+1 hour1h 21m
6+6 hours7h 21m

Sau khi attempt 6 fail, delivery được chuyển vào dead letter và email tài khoản merchant được thông báo. Event bị dead-letter có thể replay từ panel Developers → Webhooks → Delivery history trong dashboard, hoặc trực tiếp qua POST /v1/webhooks/deliveries/:id/replay. Mỗi lần replay tạo row delivery mới với X-Delivery riêng — chuỗi audit gắn lại với event gốc qua parent_delivery_id để retry-của-replay không che event nguồn.

Đăng ký một endpoint

Từ merchant dashboard :

  1. Developers → Webhooks+ Add endpoint
  2. Paste URL của bạn — chỉ https://… (HTTP thuần bị reject; form tạo cũng chặn localhost, các dải IP riêng, và URL mang userinfo)
  3. Chọn các event để subscribe (hoặc * cho tất cả)
  4. 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)
  5. 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 duy trì trạng thái retry, secret, và circuit breaker theo từng host 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.ping ký 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. Bỏ qua pipeline RMQ nê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-SignatureX-Signature-Prev trong 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 — flip is_active 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

  1. Trả 2xx nhanh. Acknowledge với 200 OK trước khi làm việc nặng — đẩy fulfillment vào background queue. 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.
  2. Dedup trên X-Delivery (hoặc Idempotency-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.
  3. 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 bạn sẽ làm đầy retry queue.
  4. Log X-Delivery cạ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