Hành vi gửi
Lấy từ tài liệu của chính hãng, kiểm tra lần cuối 2026-08-06. Hãng không nêu con số nào thì bảng này cũng không ghi.
| Thời hạn phản hồi | 5 giây Áp dụng cho cả batch, mỗi batch chứa tối đa 100 thông báo |
|---|---|
| Thử lại | Tối đa 10 lần gửi, rải trong 24 giờ |
| Ngừng gửi | hãng không công bố |
| Nguồn | https://developers.hubspot.com/docs/api-reference/latest/webhooks/guide |
Chữ ký
Kiểm chứng bằng một bộ xác minh chạy thật, có bộ test chứng minh, chứ không đọc từ tài liệu. Kiểm chứng lần cuối 2026-08-20.
| Thuật toán | HMAC-SHA256 |
|---|---|
| Payload được ký | POST{decodedUrl}{body}{timestamp} |
| Encoding | base64 |
| Header | x-hubspot-signature-v3 |
| Dung sai | 5 phút |
Xác minh
Dùng raw body của request, đúng từng byte, trước mọi bước parse JSON. Mọi cơ chế trên trang này đều hỏng ngay khi framework serialize lại payload.
const crypto = require("node:crypto");
// v3: base64(HMAC-SHA256(secret, "POST" + url + body + timestamp))
const ts = req.headers["x-hubspot-request-timestamp"];
if (Math.abs(Date.now() - Number(ts)) > 300_000) throw new Error("stale");
const expected = crypto
.createHmac("sha256", clientSecret) // the app's client secret
.update("POST" + fullUrl + rawBody + ts)
.digest("base64");
const received = Buffer.from(req.headers["x-hubspot-signature-v3"] ?? "");
const wanted = Buffer.from(expected);
// timingSafeEqual throws on a length mismatch, so check that first.
const valid =
received.length === wanted.length && crypto.timingSafeEqual(received, wanted);fullUrl là chính xác URL công khai mà HubSpot đã gọi, gồm cả query string, sau khi URL-decode. Nếu đứng sau một proxy viết lại header Host, hãy dựng lại URL từ các header forwarded, nếu không chữ ký sẽ không bao giờ khớp.
Những chỗ dễ vấp
Thời hạn 5 giây tính cho mỗi request, và một request có thể chứa batch tối đa 100 thông báo. Xử lý chúng ngay trong request nghĩa là mỗi thông báo thực chất chỉ có 50 ms.
Tài liệu của HubSpot ghi eventId không được đảm bảo là duy nhất và không hứa hẹn gì về thứ tự, nên thói quen chống trùng lặp theo event id như với Stripe không áp dụng được.
Đọc thêm
AnyHook đứng trước các endpoint nhận webhook từ HubSpot: trả lời sau khoảng 1 giây, tự thử lại theo lịch riêng khi server của bạn ngừng hoạt động, và giữ mọi sự kiện để bạn phát lại được. Đổi một URL, giữ nguyên code; để nhận được các lần thử lại và phát lại, bạn xác minh AnyHook-Signature, vì chữ ký riêng của HubSpot hết hạn sau 5 phút.