Your endpoint receives HubSpot events but your computed hash never matches
X-HubSpot-Signature-v3. HubSpot's current scheme signs more than the body:
the HMAC covers the HTTP method, the full request URI, the raw body, and a
timestamp, concatenated in that order. Any one of those four being slightly off
— a rewritten URL behind a proxy, a re-serialized JSON body, a stale timestamp — kills
the match, even when your HMAC code is perfect.
X-HubSpot-Signature-v3 is a base64 HMAC-SHA256 over the
UTF-8 string requestMethod + requestUri + requestBody + timestamp, where the
timestamp comes from the X-HubSpot-Request-Timestamp header (milliseconds).
The key is your app's client secret. Two extra rules from the docs that
snippets tend to skip:
X-HubSpot-Request-Timestamp to your clock. If your server's clock drifts,
valid requests fail your own freshness check.%3A %2F %3F %40 %21 %24 %27 %28 %29 %2A %2C
%3B (i.e. : / ? @ ! $ ' ( ) * , ;). The ? that starts the
query string stays as-is.Same family of bug as Square: the URL is inside the signed content. The classic mismatches:
Host or X-Forwarded-* and gets http://… or an
internal hostname instead of the public https://… URL configured in your
app's webhook settings. Build the string from configuration, not from the request.HubSpot delivers events as a JSON array of up to 100 notifications per
request, batching aggressively when lots of events fire at once (imports,
bulk edits). If your framework parses that array and you rebuild it with
JSON.stringify(req.body), key order or unicode escaping can differ from the
bytes HubSpot signed. Verify against the raw request body, captured
before any body-parser runs.
Three signature versions coexist, and they're structurally different:
X-HubSpot-Signature with
X-HubSpot-Signature-Version: v1: a plain hex SHA-256 hash
(not an HMAC) of client secret + raw body. Used by CRM-event webhook
subscriptions for backwards compatibility.v2: SHA-256 hash of
secret + method + URI + body. Sent for workflow webhook actions and CRM
cards. Query parameters must be in the exact original order.X-HubSpot-Signature-v3: the HMAC described above,
plus the timestamp. Verify this one going forward.Copying a v1 snippet and pointing it at the v3 header (or vice versa) fails forever — one is a bare hash, the other an HMAC with a timestamp.
Because the URI is inside the signed content, generic HMAC testers can never validate a HubSpot v3 signature — they hash only the body. A Hookden bin can, because the bin knows the exact URL the request hit, which is what HubSpot signed when the webhook target URL points straight at the bin:
X-HubSpot-* header —
compare byte lengths with what your server logged to catch body-parser mangling.Verifying on localhost? Relay captures to your
machine — the relay re-delivers the byte-identical body and the
X-HubSpot-* headers. Note that for v3 your local verifier should keep using
the bin URL as the URI input, since that's what HubSpot signed.
Related: Square signature verification failed (the other URL-signing scheme), Stripe signature verification failed, and testing webhook retry behavior.
No signup needed. Or from your terminal: curl https://hookden.pages.dev/new
← All guides · Signature debugger · Docs · Hookden vs webhook.site