Pular para o conteúdo principal

HubSpot Webhooks: Signature, Idempotency & Dead-Letter

hubspot webhooks

Running HubSpot webhooks in production comes down to three disciplines: validate every request with the v3 signature (HMAC SHA-256 over method + URI + body + timestamp), treat delivery as at-least-once and out-of-order (so your handler must be idempotent), and persist events before processing, because HubSpot retries failed deliveries only 10 times over 24 hours and offers no dead-letter queue: after that window, the event is gone. This guide covers the three, with the exact rules, the code, and the failure modes we've debugged in real portals.

The facts that shape everything (verified against HubSpot's docs and community):
  • Signature v3: HMAC SHA-256, Base64-encoded, headers X-HubSpot-Signature-v3 + X-HubSpot-Request-Timestamp; reject timestamps older than 5 minutes.
  • Response window: your endpoint must respond within ~5 seconds; batches carry up to 100 events; concurrency defaults to 10.
  • Delivery: at-least-once (duplicates happen) and ordering is not guaranteed; sequence by occurredAt.
  • Retries: up to 10 attempts over 24 hours, triggered by timeouts and any 4xx/5xx response; not configurable, no manual replay.

 

Signature validation: v1, v2, v3, and why raw bytes matter

HubSpot signs webhook requests so your endpoint can prove they actually came from HubSpot and weren't tampered with. There are three versions in the wild, and knowing which one applies where saves hours of head-scratching:

  • v1 (X-HubSpot-Signature, version header = v1): SHA-256 hash of client_secret + request_body, hex-encoded. Used by classic CRM object webhook subscriptions.
  • v2: SHA-256 hash of client_secret + http_method + URI + request_body. Used by workflow webhook actions and CRM cards.
  • v3 (X-HubSpot-Signature-v3): HMAC SHA-256 with the client secret as key, over method + URI + body + timestamp, Base64-encoded, with the timestamp arriving in X-HubSpot-Request-Timestamp. This is the current recommendation, and the only version with replay protection: reject any request whose timestamp is older than 5 minutes.

A working v3 validation in Node, using the raw request body:

const crypto = require('crypto');  function isValidHubSpotRequest(req, rawBody, clientSecret) {   const timestamp = req.headers['x-hubspot-request-timestamp'];   // HubSpot timestamps are in MILLISECONDS   if (Date.now() - parseInt(timestamp, 10) > 5 * 60 * 1000) return false;    const uri = `https://${req.headers.host}${req.url}`;   const base = `${req.method}${uri}${rawBody}${timestamp}`;   const expected = crypto     .createHmac('sha256', clientSecret)     .update(base, 'utf8')     .digest('base64');    const received = req.headers['x-hubspot-signature-v3'];   return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received)); } 

Four details cause virtually all "signature mismatch" tickets:

  • Validate the raw bytes, not the parsed body. If any middleware parses and re-serializes JSON, trims strings, or normalizes whitespace before validation, the hash changes and validation fails intermittently, only on payloads containing whatever the middleware "fixes". A real case from the HubSpot Community: a global hook trimmed input strings, one webhook value had a trailing space, and signatures mismatched "randomly" for months. Capture the raw body before any processing.
  • Reconstruct the URI exactly as HubSpot called it. Behind proxies and load balancers, scheme, host, or port can differ from what your framework reports. If validation fails consistently, log the URI you're building and compare it with the target URL configured in the app.
  • The timestamp is in milliseconds. Comparing it against seconds-based clocks makes every request look ancient and everything gets rejected.
  • Use a timing-safe comparison for the final check, as in the snippet, to avoid leaking signature information byte by byte.

The client secret used as the HMAC key is your app's secret, the same one from your app's auth settings; our guide to HubSpot API authentication and app setup covers where it lives and how to store it safely.

The delivery model: at-least-once, out of order, and in batches

Designing the handler starts with accepting three properties of HubSpot's delivery model that you cannot change:

  • At-least-once delivery. Duplicates are not a bug; they're part of the contract. A delivery that times out after your server actually processed it will be retried, and you'll see the same event again.
  • No ordering guarantee. A propertyChange event can arrive before the creation event of the same record. Sequence logic must use the occurredAt timestamp inside each event, never arrival order.
  • Batches of up to 100 events. One HTTP request may carry a mix of subscription types and records. Process events individually inside the batch, so one bad event doesn't fail the other 99 (more on that in the dead-letter section).

And the operational constraint that shapes the architecture: respond fast, within about 5 seconds. The pattern that survives production is ack-then-process: validate the signature, persist the raw events, return 200 immediately, and do the real work (API calls, enrichment, writes back to the CRM via API) asynchronously from a queue. A handler that does heavy processing inline will time out under load, trigger retries, and amplify its own traffic exactly when things are worst.

Idempotency: the discipline that makes duplicates harmless

Since duplicates are guaranteed eventually, the handler must produce the same final state no matter how many times an event arrives. The practical recipe:

  • Build a deduplication key from eventId + portalId (adding attemptNumber if you want to track redeliveries separately) and record processed keys in a store with a TTL longer than the 24-hour retry window. Seeing a key twice? Ack and skip.
  • Make writes conditional. Search-before-create for records, upsert semantics where available, and state checks before side effects ("is this deal already marked synced?"). The same principle we apply to custom code actions that get retried: anything that can run twice must be safe to run twice.
  • Never fire non-idempotent side effects directly from the event. Emails, invoices, and notifications go through the deduplicated queue, not straight from the HTTP handler.
  • Resolve races with occurredAt. When two property changes for the same record arrive out of order, last-write-wins by event timestamp, not by arrival time.

Dead-letter: the queue HubSpot doesn't give you

Here is the fact that should drive your architecture: HubSpot retries a failed delivery up to 10 times over 24 hours, and then the event is gone. There is no dead-letter queue on HubSpot's side, no dashboard of failed deliveries to replay, no manual retry button. If your endpoint was down for a deploy gone wrong on Friday night and stayed down through Sunday, Monday's data is simply missing, unless you built for it.

Building for it means two layers:

  • Persist first, process later. The HTTP handler's only jobs are validate, store the raw event, and ack. With every event durably stored before processing, a processing bug never loses data: you fix the bug and re-run from your own store. This turns HubSpot's 24-hour window into "forever" for every event that reached you.
  • Your own dead-letter queue for processing failures. Events that fail processing after N internal retries go to a DLQ table or queue with the error attached, alerting included. Crucially, process events individually: with batches of up to 100, a naive handler that returns 500 because one event was malformed forces redelivery of all 100, multiplying duplicates and load.

And for the gap that persistence can't cover (events emitted while your endpoint was completely unreachable beyond the retry window), the recovery is reconciliation by backfill: a scheduled job that queries the Search API for records modified since the last known-good sync and heals the delta. Webhooks keep you real-time; the backfill keeps you honest. We covered the sync mechanics in syncing recently updated contacts via API, and the broader event architecture in automating HubSpot with webhooks, workflows, and custom code.

A tip from someone who has been burned: the worst webhook incidents we've rescued had the same shape: nobody noticed deliveries were failing until the retry window had expired. HubSpot won't page you when your endpoint 500s. Monitor from your side: alert on error rate at the endpoint, on queue depth, and, the underrated one, on silence. If a subscription that normally delivers hundreds of events per hour delivers zero for thirty minutes, something upstream broke, and every quiet minute is data you may be losing.

Production checklist

  • v3 signature validated on the raw body, timing-safe comparison, timestamp rejected after 5 minutes (in milliseconds).
  • Ack within 5 seconds: validate, persist raw event, return 200; all processing async.
  • Deduplication key (eventId + portalId) with TTL beyond 24 hours.
  • Events processed individually within batches; one bad event never fails the other 99.
  • Ordering by occurredAt, never by arrival.
  • Internal DLQ with error context and alerting for processing failures.
  • Scheduled backfill reconciliation for outages beyond the retry window.
  • Monitoring on error rate, queue depth, and event silence.

Frequently asked questions

How do I validate a HubSpot webhook signature?

Use v3: compute an HMAC SHA-256 (key = your app's client secret) over the concatenation of HTTP method, full URI, raw request body, and the value of the X-HubSpot-Request-Timestamp header, Base64-encode it, and compare it to the X-HubSpot-Signature-v3 header using a timing-safe comparison. Reject requests whose timestamp is older than 5 minutes, and always validate the raw body before any middleware touches it.

How many times does HubSpot retry a failed webhook?

Up to 10 times over 24 hours, triggered by connection failures, timeouts, and any 4xx or 5xx response. The schedule is not configurable, there is no manual replay, and after the window expires the event is not delivered again, which is why persisting events on arrival and having a backfill routine matters.

Are HubSpot webhooks delivered in order?

No. Delivery is at-least-once with no ordering guarantee, and a single request can batch up to 100 events of mixed types. Use each event's occurredAt timestamp to sequence changes, and deduplicate using eventId plus portalId, because the same event can legitimately arrive more than once.

Why does my HubSpot webhook signature validation fail intermittently?

Almost always because something modified the body between HubSpot and your validation: JSON re-serialization, string trimming, whitespace normalization, or encoding changes by middleware. Validate against the raw request bytes, reconstruct the URI exactly as configured (watch proxies changing scheme or host), and remember the timestamp header is in milliseconds.

Does HubSpot have a dead-letter queue for webhooks?

No. After the 10 retries over 24 hours, failed deliveries are gone, with no replay mechanism on HubSpot's side. Production integrations persist every event before processing, run their own dead-letter queue for processing failures, and schedule reconciliation backfills via the Search API to heal any gaps from extended outages.

Ready to take your operation to the next level?

Talk to a specialist and see how we can help.

paper-plane