Ba phần: giao (delivery), xác thực (authenticity), ngữ nghĩa (semantics).
Giao — at-least-once, không bao giờ exactly-once.
- Ghi event vào bảng outbox trong cùng transaction nghiệp vụ, worker riêng đọc outbox rồi gửi đi. Nếu gửi HTTP ngay trong transaction thì rollback vẫn đã bắn webhook.
- Retry theo exponential backoff có jitter khi gặp lỗi mạng hoặc 5xx/429. Tôn trọng Retry-After nếu bên nhận trả về. GitHub cho redeliver thủ công trong 3 ngày — nên có cơ chế tương đương.
- 4xx (trừ 429) là lỗi cố định của bên nhận → đừng retry vô hạn, đánh dấu fail.
- Hết số lần retry thì đưa vào dead-letter và cho phép replay thủ công; tự động tắt endpoint liên tục lỗi để không giữ hàng đợi.
- Timeout ngắn (vài giây) và kỳ vọng bên nhận chỉ 2xx rồi xử lý bất đồng bộ.
Xác thực — HMAC chứ không phải mật khẩu trong body.
- Mỗi endpoint một secret. Ký HMAC-SHA256(secret, timestamp + "." + raw_body), gửi qua header kiểu X-Signature: t=1735.,v1=abc....
- Ký trên raw body, không phải JSON đã parse lại — re-serialize làm đổi byte và chữ ký sẽ sai.
- Bên nhận phải so sánh bằng hàm constant-time (crypto.timingSafeEqual), tránh timing attack.
- Hỗ trợ hai secret cùng lúc để xoay khoá không mất event.
Chống replay.
- Đưa timestamp vào phần được ký và bên nhận từ chối nếu lệch quá ngưỡng (thường 5 phút) — kẻ tấn công chụp lại request cũ không dùng lại được.
- Mỗi lần gửi có event_id ổn định (redeliver giữ nguyên id), bên nhận lưu id đã xử lý để khử trùng lặp.
Ngữ nghĩa. Event có thể tới không đúng thứ tự — nên kèm sequence/occurred_at và để bên nhận bỏ qua event cũ hơn trạng thái hiện tại. Payload nên mỏng (id + loại event) rồi bên nhận gọi API lấy trạng thái mới nhất, tránh việc xử lý dữ liệu đã lỗi thời và tránh rò dữ liệu nhạy cảm qua log của bên thứ ba.