HubSpot webhooks: app subscriptions, workflow webhooks and signature v3

To test HubSpot webhooks, point an app's target URL or a workflow webhook action at a free webhooks.sh bin (no signup, 48 hours, 100 requests a day) and watch each request arrive live. App webhooks batch up to 100 events per request and retry up to 10 times over 24 hours.

HubSpot has two webhook features: app webhooks that POST batches of CRM events to a target URL, and the workflow Send a webhook action that sends a request when an enrolled record reaches that step. Both can point at a free bin so you can inspect the exact headers and payload HubSpot sends. This page covers choosing between them, capturing app batches and workflow requests, checking X-HubSpot-Signature-v3, and how HubSpot handles retries.

App webhooks or workflow webhooks?

If you're a developer building an integration, use app webhooks; if you're an admin automating a process in your own HubSpot account, use the workflow action.

FeatureApp webhooksWorkflow Send a webhook
Set up inApp webhook settings or project configAutomation > Workflows
RequiresA HubSpot app with subscriptionsData Hub Professional or Enterprise
RequestPOST, JSON array of up to 100 eventsPOST with JSON, or GET
SignatureX-HubSpot-Signature-v3v2 signature header, or an API key
Built-in testTest this subscriptionTest action

Send app webhooks to a free bin

  1. Create a free bin in your browser at view.webhooks.page or run curl -X POST https://webhooks.sh/v1/bins to get your ingest_url and view_url.
  2. Set the bin's ingest URL (https://<bin>.webhooks.page/) as the target URL: for private apps, go to Settings > Integrations > Private apps > your app > Webhooks tab, and for project-based apps, set settings.targetUrl in the webhooks config.
  3. Subscribe to an event such as contact.creation and click Test this subscription, or create a contact in HubSpot.
  4. Watch the batch arrive live in the bin viewer.

Changes to the webhook URL or subscription settings can take up to 5 minutes to take effect because HubSpot caches webhook settings.

# Create a free bin (or open view.webhooks.page)
curl -X POST https://webhooks.sh/v1/bins

# Use the ingest_url as the app's target URL, e.g.
# https://<bin>.webhooks.page/hubspot

What a batch looks like

Each app webhook request body is a JSON array of up to 100 events. Every event includes subscriptionType (like contact.creation or contact.propertyChange), objectId, occurredAt (a timestamp in milliseconds), and attemptNumber (which starts at 0 and counts retries), while propertyChange events also carry propertyName and propertyValue. HubSpot can send duplicate events, doesn't guarantee delivery order, and notes in its docs that eventId isn't guaranteed to be unique, so make your handler idempotent and check occurredAt to skip stale updates.

[
  {
    "eventId": 1,
    "subscriptionId": 12345,
    "portalId": 62515,
    "appId": 54321,
    "occurredAt": 1564113600000,
    "subscriptionType": "contact.propertyChange",
    "attemptNumber": 0,
    "objectId": 123,
    "propertyName": "email",
    "propertyValue": "ada@example.com",
    "changeSource": "CRM"
  }
]

Verify X-HubSpot-Signature-v3

To verify X-HubSpot-Signature-v3, concatenate the HTTP method, the full request URI (https:// plus host, path, and query), the raw request body, and the X-HubSpot-Request-Timestamp header value, then compute the HMAC-SHA256 using your app's client secret, base64-encode the result, and compare it to the signature in constant time. Decode any URL-encoded characters from the list in HubSpot's docs in the URI before hashing, and reject requests where X-HubSpot-Request-Timestamp is more than 5 minutes old. Both headers appear in the webhooks.sh bin viewer alongside the exact path and timestamp HubSpot signed.

Workflow webhooks and app cards use signature v2 instead, which is flagged by X-HubSpot-Signature-Version: v2 and sets X-HubSpot-Signature to the SHA-256 hex digest of your client secret + method + URI + body (see the webhook security guide for more on signature verification).

import { createHmac, timingSafeEqual } from "node:crypto";

export function isFromHubSpot(req, rawBody, clientSecret) {
  const ts = req.headers["x-hubspot-request-timestamp"];
  if (Date.now() - Number(ts) > 5 * 60 * 1000) return false;

  const uri = `https://${req.headers.host}${req.url}`;
  const expected = createHmac("sha256", clientSecret)
    .update(req.method + uri + rawBody + ts)
    .digest("base64");
  const a = Buffer.from(expected);
  const b = Buffer.from(req.headers["x-hubspot-signature-v3"] ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}

Test a workflow webhook action

  1. In HubSpot, go to Automation > Workflows and add the Send a webhook action from the Data ops section.
  2. Choose POST, paste your bin's ingest URL (HubSpot requires HTTPS, which https://<bin>.webhooks.page/ uses), and pick an authentication method: either Include request signature in header, which needs your HubSpot App ID, or an API key sent as a header or query parameter.
  3. Click Test action to see the bin's 200 response in HubSpot while the request pops up in your bin viewer.
  4. To capture a live request, turn the workflow on and enroll a test record.

If you send an API key in a header whose name contains key (or authorization, cookie, token, or secret), its value shows as redacted in the viewer.

Retries, timeouts and limits

A free bin responds with HTTP 200 right away, so HubSpot won't retry against it during normal testing, but a busy HubSpot account can burn through the free limit of 100 requests per day quickly. Once a bin hits 100 requests in a day, it returns 429 (daily_cap_reached), which HubSpot treats as a failed delivery and retries (see the webhook retries guide). Test from a quiet account, or use Solo if you're dealing with real volume. Here is how HubSpot's limits and retry rules work:

RuleValue
Events per requestUp to 100
Response deadline5 seconds per batch
RetriesUp to 10, spread over 24 hours
Retried onConnection failure, timeout, any 4xx or 5xx
Concurrency limit10 in-flight requests per account
Subscriptions per appUp to 1,000
Signature timestamp window5 minutes
Settings cacheUp to 5 minutes

Keep the URL or move to localhost

You can claim a free bin from the bin page with a free Google sign-in to stop the 48-hour expiry timer and keep 30 days of rolling request history at the same URL. If you need more capacity or local forwarding, Solo ($12 a month on the pricing page) gives you a persistent named endpoint on webhooks.host with 250,000 requests a month, and the wsh CLI (wsh listen <endpoint> --to localhost:3000) relays live deliveries straight to your handler on localhost.

FAQ

How do I test HubSpot webhooks without deploying anything?

Point your HubSpot app's target URL or your workflow webhook URL at a free bin from https://view.webhooks.page/, then click Test this subscription or Test action, or trigger an event in your account to see the request arrive in the browser.

Which HubSpot plan includes workflow webhooks?

The Send a webhook workflow action requires Data Hub Professional or Enterprise.

How many events does HubSpot send per webhook request?

App webhooks batch up to 100 events into a single JSON array per HTTP request.

How do I validate X-HubSpot-Signature-v3?

Compute the base64 HMAC-SHA256 of the HTTP method + full URI + raw body + X-HubSpot-Request-Timestamp header using your app's client secret, compare it in constant time to X-HubSpot-Signature-v3, and reject timestamps older than 5 minutes.

Does HubSpot retry failed webhooks?

Yes, HubSpot retries failed notifications up to 10 times over the next 24 hours on connection failures, timeouts over 5 seconds, and any 4xx or 5xx response.

Why do I get the same HubSpot event twice?

HubSpot can deliver the same event more than once, doesn't guarantee order, and notes that eventId isn't guaranteed to be unique, so make your handler idempotent and check occurredAt before applying updates.

Last updated

Try it in 10 seconds.

Create a free bin at view.webhooks.page — no signup, no install. Point your sender at the URL and watch requests land live. Bins expire after 48 hours.