HubSpot webhook signature validation failing? Check what v3 actually signs

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.

What v3 actually signs

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:

Cause 1: the URI you verify with ≠ the URI HubSpot signed

Same family of bug as Square: the URL is inside the signed content. The classic mismatches:

Cause 2: re-serialized body — HubSpot sends batches

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.

Cause 3: wrong secret or stale settings

Cause 4: v1 / v2 / v3 confusion

Three signature versions coexist, and they're structurally different:

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.

Not a signature problem? Delivery facts worth knowing

Isolate it in one minute with a capture URL

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:

  1. Create a bin and set it as the target URL in your app's webhook settings (allow up to 5 minutes for the settings cache).
  2. In the bin's settings, choose HubSpot as the signature scheme and paste the app's client secret.
  3. Trigger a test event. Every capture shows a ✓/✗ badge: ✓ means the secret is right and HubSpot's signatures verify — so the failure is in your verifier's inputs (almost always Cause 1 or 2). ✗ on a fresh capture means the secret is wrong — the URI can't be the problem here, since the bin verifies with the URL it was actually called on.
  4. The capture shows the raw batch body and every 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