Skip to content

HubSpot Integration

Receive and route HubSpot webhooks for contacts, companies, deals, and other CRM events.

Batched Payloads

HubSpot sends webhook events as a JSON array (batch), even when delivering a single event. Your transforms and filters should account for this array structure.

Setup

1. Create a Source in Hookbase

bash
curl -X POST https://api.hookbase.app/api/sources \
  -H "Authorization: Bearer whr_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "HubSpot Production",
    "slug": "hubspot",
    "verificationConfig": {
      "type": "hmac",
      "secret": "your-hubspot-client-secret",
      "algorithm": "sha256",
      "header": "X-HubSpot-Signature-v3",
      "encoding": "base64"
    }
  }'

Save your webhook URL:

https://api.hookbase.app/ingest/{orgSlug}/hubspot

2. Configure HubSpot Webhooks

HubSpot webhooks are configured through Apps in a HubSpot Developer Account:

  1. Go to HubSpot Developer and sign in
  2. Navigate to Apps → select your app (or create one)
  3. Go to the Webhooks tab
  4. Set the Target URL to your Hookbase webhook URL
  5. Click Create subscription for each event you want to receive
  6. Select the object type (Contact, Company, Deal) and event type (Created, Updated, Deleted)
  7. Activate your subscriptions

TIP

Your app's Client secret is used for signature verification. Find it under AuthClient secret in your app settings.

3. Create Routes

bash
# Create destination for your CRM handler
curl -X POST https://api.hookbase.app/api/destinations \
  -H "Authorization: Bearer whr_your_api_key" \
  -d '{"name": "CRM Service", "url": "https://api.myapp.com/webhooks/hubspot"}'

# Create route
curl -X POST https://api.hookbase.app/api/routes \
  -H "Authorization: Bearer whr_your_api_key" \
  -d '{"name": "HubSpot to CRM Service", "sourceId": "src_...", "destinationIds": ["dst_..."]}'

Signature Verification

HubSpot uses v3 signature verification with HMAC-SHA256. The signed content is a concatenation of:

client_secret + HTTP_METHOD + URL + request_body + timestamp

The signature is sent in the X-HubSpot-Signature-v3 header as a base64-encoded string, and the timestamp is in the X-HubSpot-Request-Timestamp header.

Hookbase verifies this signature automatically when configured:

json
{
  "verificationConfig": {
    "type": "hmac",
    "secret": "your-hubspot-client-secret",
    "algorithm": "sha256",
    "header": "X-HubSpot-Signature-v3",
    "encoding": "base64"
  }
}

Signature Complexity

HubSpot's v3 signature includes the full request URL and HTTP method in the signed content, which makes it more complex than standard HMAC body-only verification. If you encounter issues with signature verification, you can initially use a shared secret via a custom header while troubleshooting.

Replay Attack Prevention

Always validate the X-HubSpot-Request-Timestamp header to prevent replay attacks. Reject any request where the timestamp is older than 5 minutes. Hookbase checks this automatically when timestamp tolerance is configured.

Common Events

HubSpot sends events as a JSON array. Each element in the array represents a single event.

contact.creation

Triggered when a new contact is created in HubSpot.

json
[
  {
    "objectId": 1246965,
    "propertyName": "lifecyclestage",
    "propertyValue": "subscriber",
    "changeSource": "CRM_UI",
    "eventId": 3816279340,
    "subscriptionId": 294235,
    "portalId": 62515,
    "appId": 54321,
    "occurredAt": 1704067200000,
    "subscriptionType": "contact.creation",
    "attemptNumber": 0
  }
]

contact.propertyChange

Triggered when a contact property is updated.

json
[
  {
    "objectId": 1246965,
    "propertyName": "email",
    "propertyValue": "[email protected]",
    "changeSource": "CRM_UI",
    "eventId": 3816279341,
    "subscriptionId": 294236,
    "portalId": 62515,
    "appId": 54321,
    "occurredAt": 1704067260000,
    "subscriptionType": "contact.propertyChange",
    "attemptNumber": 0
  }
]

deal.creation

Triggered when a new deal is created.

json
[
  {
    "objectId": 9843210,
    "propertyName": "dealstage",
    "propertyValue": "appointmentscheduled",
    "changeSource": "CRM_UI",
    "eventId": 3816279342,
    "subscriptionId": 294237,
    "portalId": 62515,
    "appId": 54321,
    "occurredAt": 1704067320000,
    "subscriptionType": "deal.creation",
    "attemptNumber": 0
  }
]

deal.propertyChange

Triggered when a deal property (e.g., stage, amount) is updated.

json
[
  {
    "objectId": 9843210,
    "propertyName": "dealstage",
    "propertyValue": "closedwon",
    "changeSource": "CRM_UI",
    "eventId": 3816279343,
    "subscriptionId": 294238,
    "portalId": 62515,
    "appId": 54321,
    "occurredAt": 1704067380000,
    "subscriptionType": "deal.propertyChange",
    "attemptNumber": 0
  }
]

company.creation

Triggered when a new company is created.

json
[
  {
    "objectId": 5678432,
    "propertyName": "name",
    "propertyValue": "Acme Corp",
    "changeSource": "INTEGRATION",
    "eventId": 3816279344,
    "subscriptionId": 294239,
    "portalId": 62515,
    "appId": 54321,
    "occurredAt": 1704067440000,
    "subscriptionType": "company.creation",
    "attemptNumber": 0
  }
]

HubSpot Subscription Types

CategorySubscription Types
Contactscontact.creation, contact.propertyChange, contact.deletion
Companiescompany.creation, company.propertyChange, company.deletion
Dealsdeal.creation, deal.propertyChange, deal.deletion

Transform Examples

Slack Alert for New Deals

Since HubSpot sends batched events, iterate over the array and format each event:

javascript
function transform(payload) {
  // payload is an array of events
  const event = payload[0];
  const timestamp = new Date(event.occurredAt).toISOString();

  return {
    blocks: [
      {
        type: "header",
        text: {
          type: "plain_text",
          text: "New Deal Created in HubSpot"
        }
      },
      {
        type: "section",
        fields: [
          {
            type: "mrkdwn",
            text: `*Deal Stage:*\n${event.propertyValue}`
          },
          {
            type: "mrkdwn",
            text: `*Object ID:*\n${event.objectId}`
          }
        ]
      },
      {
        type: "section",
        fields: [
          {
            type: "mrkdwn",
            text: `*Source:*\n${event.changeSource}`
          },
          {
            type: "mrkdwn",
            text: `*Time:*\n${timestamp}`
          }
        ]
      },
      {
        type: "context",
        elements: [
          {
            type: "mrkdwn",
            text: `Event ID: ${event.eventId} | Portal: ${event.portalId}`
          }
        ]
      }
    ]
  };
}

Database Format

Flatten each batched event into a normalized record:

javascript
function transform(payload) {
  // Handle batched events — map each to a database record
  return payload.map(event => ({
    hubspot_event_id: String(event.eventId),
    subscription_type: event.subscriptionType,
    object_id: String(event.objectId),
    property_name: event.propertyName || null,
    property_value: event.propertyValue || null,
    change_source: event.changeSource,
    portal_id: String(event.portalId),
    app_id: String(event.appId),
    attempt_number: event.attemptNumber,
    occurred_at: new Date(event.occurredAt).toISOString(),
    received_at: new Date().toISOString()
  }));
}

Alert for Deal Stage Changes

javascript
function transform(payload) {
  const dealEvents = payload.filter(
    e => e.subscriptionType === 'deal.propertyChange' && e.propertyName === 'dealstage'
  );

  if (dealEvents.length === 0) return null;

  const event = dealEvents[0];
  const stageLabels = {
    appointmentscheduled: 'Appointment Scheduled',
    qualifiedtobuy: 'Qualified to Buy',
    presentationscheduled: 'Presentation Scheduled',
    decisionmakerboughtin: 'Decision Maker Bought In',
    contractsent: 'Contract Sent',
    closedwon: 'Closed Won',
    closedlost: 'Closed Lost'
  };

  const stageName = stageLabels[event.propertyValue] || event.propertyValue;

  return {
    channel: "#sales-alerts",
    username: "HubSpot Bot",
    icon_emoji: event.propertyValue === 'closedwon' ? ":tada:" : ":briefcase:",
    attachments: [{
      color: event.propertyValue === 'closedwon' ? "good" : "#3498db",
      title: `Deal Stage Changed: ${stageName}`,
      fields: [
        { title: "Deal ID", value: String(event.objectId), short: true },
        { title: "New Stage", value: stageName, short: true },
        { title: "Source", value: event.changeSource, short: true },
        { title: "Portal", value: String(event.portalId), short: true }
      ],
      footer: `Event ${event.eventId}`,
      ts: Math.floor(event.occurredAt / 1000)
    }]
  };
}

Filter Examples

Contact Events Only

json
{
  "name": "Contact Events",
  "logic": "or",
  "conditions": [
    {
      "field": "[0].subscriptionType",
      "operator": "starts_with",
      "value": "contact."
    }
  ]
}

Deal Events Only

json
{
  "name": "Deal Events",
  "logic": "or",
  "conditions": [
    {
      "field": "[0].subscriptionType",
      "operator": "starts_with",
      "value": "deal."
    }
  ]
}

Closed Won Deals

json
{
  "name": "Closed Won Deals",
  "logic": "and",
  "conditions": [
    {
      "field": "[0].subscriptionType",
      "operator": "equals",
      "value": "deal.propertyChange"
    },
    {
      "field": "[0].propertyName",
      "operator": "equals",
      "value": "dealstage"
    },
    {
      "field": "[0].propertyValue",
      "operator": "equals",
      "value": "closedwon"
    }
  ]
}

CRM UI Changes Only

Filter to only events triggered by manual changes in the HubSpot UI:

json
{
  "name": "Manual CRM Changes",
  "logic": "and",
  "conditions": [
    {
      "field": "[0].changeSource",
      "operator": "equals",
      "value": "CRM_UI"
    }
  ]
}

Headers

HubSpot sends these headers with webhooks:

HeaderDescription
X-HubSpot-Signature-v3HMAC-SHA256 signature (base64-encoded)
X-HubSpot-Request-TimestampUnix timestamp (milliseconds) of the request
Content-Typeapplication/json

Testing

Using HubSpot Test Endpoint

You can trigger test webhook events from your HubSpot app settings:

  1. Go to Apps → your app → Webhooks
  2. Click the Test button next to a subscription
  3. HubSpot will send a sample event to your configured URL

Create a Test Source

Create a separate source for development:

bash
curl -X POST https://api.hookbase.app/api/sources \
  -H "Authorization: Bearer whr_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "HubSpot Dev",
    "slug": "hubspot-dev",
    "verificationConfig": {
      "type": "hmac",
      "secret": "your-dev-app-client-secret",
      "algorithm": "sha256",
      "header": "X-HubSpot-Signature-v3",
      "encoding": "base64"
    }
  }'

Best Practices

  1. Handle batched payloads: Always treat the incoming payload as an array — HubSpot batches events even when sending a single one

  2. Use event IDs for idempotency: Each event has a unique eventId; use it to prevent duplicate processing

  3. Check attemptNumber: HubSpot retries failed deliveries and increments this field — ensure your handlers are idempotent

  4. Separate dev and production: Use different HubSpot apps (and therefore different client secrets) for development and production

  5. Subscribe selectively: Only subscribe to the object types and events you need to reduce noise

  6. Validate timestamps: Check X-HubSpot-Request-Timestamp and reject requests older than 5 minutes to prevent replay attacks

  7. Monitor changeSource: Use the changeSource field to distinguish between UI changes, API changes, and integration-triggered changes

Troubleshooting

Signature Verification Failed

  1. Ensure you're using the Client secret from your HubSpot app (under Auth settings), not the API key
  2. HubSpot v3 signatures include the full URL and HTTP method in the signed content — verify the webhook URL matches exactly
  3. Check that the timestamp tolerance is not too strict; HubSpot requests may have slight delays
  4. If you continue to have issues, temporarily disable verification and inspect the raw headers to confirm the signature format

Events Not Arriving

  1. Check that your webhook subscriptions are active in the HubSpot app settings
  2. Verify your app is installed on the target HubSpot portal
  3. Confirm the target URL is publicly accessible
  4. Check HubSpot's webhook logs under MonitoringWebhooks in your developer account

Unexpected Payload Format

  1. Remember that HubSpot sends events as a JSON array, not a single object
  2. Batch size can vary — always iterate over the array
  3. The propertyName and propertyValue fields may be null for creation and deletion events

Duplicate Events

  1. HubSpot retries failed deliveries — check the attemptNumber field
  2. Enable deduplication using the eventId field:
bash
curl -X PATCH .../sources/src_hubspot \
  -d '{"dedupEnabled": true, "dedupStrategy": "provider_id"}'

Missing Property Changes

  1. HubSpot only sends events for properties you've subscribed to
  2. To receive all property changes, subscribe to contact.propertyChange (or company.propertyChange, deal.propertyChange) without specifying individual properties
  3. Check that the property exists and is not a calculated or read-only property

Released under the MIT License.