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
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}/hubspot2. Configure HubSpot Webhooks
HubSpot webhooks are configured through Apps in a HubSpot Developer Account:
- Go to HubSpot Developer and sign in
- Navigate to Apps → select your app (or create one)
- Go to the Webhooks tab
- Set the Target URL to your Hookbase webhook URL
- Click Create subscription for each event you want to receive
- Select the object type (Contact, Company, Deal) and event type (Created, Updated, Deleted)
- Activate your subscriptions
TIP
Your app's Client secret is used for signature verification. Find it under Auth → Client secret in your app settings.
3. Create Routes
# 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 + timestampThe 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:
{
"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.
[
{
"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.
[
{
"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.
[
{
"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.
[
{
"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.
[
{
"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
| Category | Subscription Types |
|---|---|
| Contacts | contact.creation, contact.propertyChange, contact.deletion |
| Companies | company.creation, company.propertyChange, company.deletion |
| Deals | deal.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:
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:
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
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
{
"name": "Contact Events",
"logic": "or",
"conditions": [
{
"field": "[0].subscriptionType",
"operator": "starts_with",
"value": "contact."
}
]
}Deal Events Only
{
"name": "Deal Events",
"logic": "or",
"conditions": [
{
"field": "[0].subscriptionType",
"operator": "starts_with",
"value": "deal."
}
]
}Closed Won Deals
{
"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:
{
"name": "Manual CRM Changes",
"logic": "and",
"conditions": [
{
"field": "[0].changeSource",
"operator": "equals",
"value": "CRM_UI"
}
]
}Headers
HubSpot sends these headers with webhooks:
| Header | Description |
|---|---|
X-HubSpot-Signature-v3 | HMAC-SHA256 signature (base64-encoded) |
X-HubSpot-Request-Timestamp | Unix timestamp (milliseconds) of the request |
Content-Type | application/json |
Testing
Using HubSpot Test Endpoint
You can trigger test webhook events from your HubSpot app settings:
- Go to Apps → your app → Webhooks
- Click the Test button next to a subscription
- HubSpot will send a sample event to your configured URL
Create a Test Source
Create a separate source for development:
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
Handle batched payloads: Always treat the incoming payload as an array — HubSpot batches events even when sending a single one
Use event IDs for idempotency: Each event has a unique
eventId; use it to prevent duplicate processingCheck
attemptNumber: HubSpot retries failed deliveries and increments this field — ensure your handlers are idempotentSeparate dev and production: Use different HubSpot apps (and therefore different client secrets) for development and production
Subscribe selectively: Only subscribe to the object types and events you need to reduce noise
Validate timestamps: Check
X-HubSpot-Request-Timestampand reject requests older than 5 minutes to prevent replay attacksMonitor
changeSource: Use thechangeSourcefield to distinguish between UI changes, API changes, and integration-triggered changes
Troubleshooting
Signature Verification Failed
- Ensure you're using the Client secret from your HubSpot app (under Auth settings), not the API key
- HubSpot v3 signatures include the full URL and HTTP method in the signed content — verify the webhook URL matches exactly
- Check that the timestamp tolerance is not too strict; HubSpot requests may have slight delays
- If you continue to have issues, temporarily disable verification and inspect the raw headers to confirm the signature format
Events Not Arriving
- Check that your webhook subscriptions are active in the HubSpot app settings
- Verify your app is installed on the target HubSpot portal
- Confirm the target URL is publicly accessible
- Check HubSpot's webhook logs under Monitoring → Webhooks in your developer account
Unexpected Payload Format
- Remember that HubSpot sends events as a JSON array, not a single object
- Batch size can vary — always iterate over the array
- The
propertyNameandpropertyValuefields may benullfor creation and deletion events
Duplicate Events
- HubSpot retries failed deliveries — check the
attemptNumberfield - Enable deduplication using the
eventIdfield:
curl -X PATCH .../sources/src_hubspot \
-d '{"dedupEnabled": true, "dedupStrategy": "provider_id"}'Missing Property Changes
- HubSpot only sends events for properties you've subscribed to
- To receive all property changes, subscribe to
contact.propertyChange(orcompany.propertyChange,deal.propertyChange) without specifying individual properties - Check that the property exists and is not a calculated or read-only property