Delivery behaviour
Read from the vendor's own documentation, last checked 2026-08-06. Where the vendor states no number, this table carries none.
| Response budget | 5s Applies to the whole batch, which can carry up to 100 notifications |
|---|---|
| Retry | Up to 10 attempts spread over 24 hours |
| Gives up | not published by the vendor |
| Source | https://developers.hubspot.com/docs/api-reference/latest/webhooks/guide |
Signature
Verified against a working verifier proven by a test suite, not read from documentation. Last verified 2026-08-20.
| Algorithm | HMAC-SHA256 |
|---|---|
| Signed payload | POST{decodedUrl}{body}{timestamp} |
| Encoding | base64 |
| Header | x-hubspot-signature-v3 |
| Tolerance | 5 min |
Verify it
The raw request body, byte for byte, before any JSON parsing. Every scheme on this page breaks the moment a framework re-serializes the 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 valid = crypto.timingSafeEqual(
Buffer.from(req.headers["x-hubspot-signature-v3"]),
Buffer.from(expected),
);fullUrl is the exact public URL HubSpot called, query string included, after URL-decoding. Behind a proxy that rewrites the Host header, reconstruct it from the forwarded headers or the signature never matches.
What bites
The 5-second budget is per request, and one request can carry a batch of up to 100 notifications. Processing them inline means your real budget is 50ms each.
HubSpot documents its eventId as not guaranteed unique and makes no ordering promise, so the Stripe habit of deduplicating on the event id does not transfer.
Read more
AnyHook sits in front of endpoints that receive from HubSpot: it answers inside the budget above, retries on its own schedule when your server is down, and keeps every event replayable. Change one URL, keep your code.