Guide to HubSpot Webhooks: Features and Best Practices
HubSpot is one of the most widely adopted CRM and marketing platforms, used by over 200,000 businesses to manage contacts, deals, tickets, and marketing automation. For developers building integrations, HubSpot's webhooks API provides a way to receive real-time notifications when CRM data changes, eliminating the need to continuously poll for updates.
This guide covers everything you need to know about HubSpot webhooks: their features, how to configure them, best practices for production deployments, and the common pain points developers face along with solutions to address them.
What are HubSpot webhooks?
HubSpot webhooks are HTTP callbacks that notify external endpoints when events occur in a HubSpot account. When a CRM record is created, updated, deleted, or associated with another record, HubSpot sends a POST request containing event details to a URL you configure. This enables real-time integration with external databases, automation platforms, notification systems, and any service that can receive HTTP requests.
Webhooks in HubSpot exist in two primary contexts:
- Webhooks API (Public Apps) — The primary webhook system where you subscribe to CRM object events at the app level. Any HubSpot account that installs your app automatically receives webhook subscriptions.
- Private App Webhooks — Webhooks configured within a private app's settings for a single HubSpot account. These can only be managed through the in-app UI, not via API.
HubSpot also supports webhook actions within Workflows, which allow outbound HTTP requests as part of automation sequences. This guide focuses primarily on the Webhooks API, as it's the most commonly used integration pattern.
HubSpot webhook features
| Feature | Details |
|---|---|
| Webhook configuration | HubSpot developer UI, API, or private app settings |
| Hashing algorithm | SHA-256 (v1, v2) and HMAC SHA-256 (v3) |
| Timeout | 5 seconds |
| Retry logic | Up to 10 retries over 24 hours |
| Batching | Up to 100 events per request |
| Concurrency limit | Configurable, minimum 5, default 10 |
| Manual retry | Not available |
| Browsable log | Limited (private app test UI only) |
| Event ordering guarantee | Not guaranteed |
| Delivery guarantee | At-least-once (duplicates possible) |
| Max subscriptions | 1,000 per application |
Supported event types
HubSpot webhooks support a comprehensive set of CRM object events across contacts, companies, deals, tickets, products, and line items:
| Event type | Description |
|---|---|
*.creation | A new record was created |
*.deletion | A record was deleted |
*.propertyChange | A specified property value changed |
*.associationChange | An association between records was added or removed |
*.restore | A record was restored from deletion |
*.merge | Two records were merged |
contact.privacyDeletion | A contact was deleted for GDPR/privacy compliance |
Each event type is available for all supported CRM objects. For example, contact.creation, company.creation, deal.creation, ticket.creation, product.creation, and line_item.creation are all valid subscription types.
HubSpot also supports conversation events (currently in beta): conversation.creation, conversation.deletion, conversation.privacyDeletion, conversation.propertyChange, and conversation.newMessage.
Webhook payload structure
HubSpot delivers webhook notifications as a JSON array of event objects. Each request can contain up to 100 events, batched together when many events occur in a short period.
[
{
"objectId": 1246965,
"propertyName": "lifecyclestage",
"propertyValue": "subscriber",
"changeSource": "ACADEMY",
"eventId": 3816279340,
"subscriptionId": 25,
"portalId": 33,
"appId": 1160452,
"occurredAt": 1462216307945,
"subscriptionType": "contact.propertyChange",
"attemptNumber": 0
},
{
"objectId": 1246978,
"changeSource": "IMPORT",
"eventId": 3816279480,
"subscriptionId": 22,
"portalId": 33,
"appId": 1160452,
"occurredAt": 1462216307945,
"subscriptionType": "contact.creation",
"attemptNumber": 0
}
]
Key payload fields
| Field | Description |
|---|---|
objectId | The ID of the CRM record that was created, changed, or deleted |
propertyName | The name of the property that changed (property change events only) |
propertyValue | The new value of the changed property |
changeSource | The source of the change (e.g., CRM, IMPORT, API, INTEGRATION) |
eventId | The ID of the event (not guaranteed to be unique) |
subscriptionId | The ID of the webhook subscription that triggered this notification |
portalId | The HubSpot account ID where the event occurred |
appId | Your application's ID |
occurredAt | When the event occurred (millisecond timestamp) |
subscriptionType | The event type (e.g., contact.creation, deal.propertyChange) |
attemptNumber | The delivery attempt number, starting at 0 |
Merge event fields
| Field | Description |
|---|---|
primaryObjectId | The ID of the merge winner (the record that remains) |
mergedObjectIds | Array of IDs for the records merged into the winner |
newObjectId | The ID of the newly created merged record |
numberOfPropertiesMoved | Count of properties transferred during the merge |
Association change fields
| Field | Description |
|---|---|
associationType | The type of association (e.g., CONTACT_TO_COMPANY, DEAL_TO_CONTACT) |
fromObjectId | The ID of the record the association change was made from |
toObjectId | The ID of the secondary record |
associationRemoved | true if an association was removed, false if created |
isPrimaryAssociation | Whether the secondary record is the primary association |
Security with request signatures
HubSpot supports three versions of request signature validation, each building on the previous:
v1 signature
Used for CRM object webhook events. HubSpot sets the X-HubSpot-Signature-Version header to v1 and computes a SHA-256 hash of:
client_secret + request_body
v2 signature
Used for workflow webhook actions and custom CRM cards. Computes a SHA-256 hash of:
client_secret + http_method + URI + request_body
v3 signature (recommended)
The latest and most secure version. Uses HMAC SHA-256 and includes a timestamp to prevent replay attacks. The X-HubSpot-Signature-v3 header contains a Base64-encoded HMAC SHA-256 hash of:
request_method + request_uri + request_body + timestamp
The timestamp is provided in the X-HubSpot-Request-Timestamp header and should be rejected if older than 5 minutes.
Setting up HubSpot webhooks
Via the HubSpot developer UI
- In your developer account, navigate to Apps and click the name of your app.
- In the left sidebar, navigate to Webhooks.
- In the Target URL field, enter the HTTPS endpoint that will receive webhooks.
- Adjust the Event throttling setting to set the maximum concurrent requests.
- Click Save.
- Click Create subscription and configure:
- Select the object types (contacts, companies, deals, etc.)
- Select the event types (creation, deletion, propertyChange, etc.)
- For property change events, select which properties to listen for
- Click Subscribe, then Activate the subscription.
New subscriptions are created in a paused state and must be explicitly activated.
Via the API
Configure webhook settings:
curl -X PUT "https://api.hubapi.com/webhooks/v3/{appId}/settings" \
-H "Authorization: Bearer $DEVELOPER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"targetUrl": "https://your-endpoint.com/webhooks/hubspot",
"throttling": {
"maxConcurrentRequests": 10
}
}'
Best practices when working with HubSpot webhooks
Verifying request signatures
Always validate the X-HubSpot-Signature-v3 header to ensure requests genuinely originated from HubSpot. Use constant-time comparison to guard against timing attacks.
Node.js
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString('utf8');
}
}));
function verifyHubSpotSignatureV3(method, uri, body, timestamp, signature, secret) {
// Reject requests older than 5 minutes
const MAX_ALLOWED_TIMESTAMP = 300000;
if (Date.now() - parseInt(timestamp) > MAX_ALLOWED_TIMESTAMP) {
return false;
}
const rawString = `${method}${uri}${body}${timestamp}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(rawString)
.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signature)
);
}
app.post('/webhooks/hubspot', (req, res) => {
const signature = req.headers['x-hubspot-signature-v3'];
const timestamp = req.headers['x-hubspot-request-timestamp'];
const uri = `https://${req.hostname}${req.originalUrl}`;
if (!verifyHubSpotSignatureV3(
'POST', uri, req.rawBody, timestamp, signature,
process.env.HUBSPOT_CLIENT_SECRET
)) {
return res.status(401).send('Invalid signature');
}
// Acknowledge immediately
res.status(200).send('OK');
// Process events asynchronously
processEvents(req.body);
});
Python
import hmac
import hashlib
import base64
import time
import os
from flask import Flask, request, abort
app = Flask(__name__)
def verify_hubspot_signature_v3(method, uri, body, timestamp, signature, secret):
# Reject requests older than 5 minutes
max_allowed = 300000 # 5 minutes in milliseconds
if int(time.time() * 1000) - int(timestamp) > max_allowed:
return False
raw_string = f"{method}{uri}{body}{timestamp}"
expected = hmac.new(
secret.encode('utf-8'),
raw_string.encode('utf-8'),
hashlib.sha256
).digest()
expected_b64 = base64.b64encode(expected).decode('utf-8')
return hmac.compare_digest(expected_b64, signature)
@app.route('/webhooks/hubspot', methods=['POST'])
def handle_hubspot_webhook():
signature = request.headers.get('X-HubSpot-Signature-v3')
timestamp = request.headers.get('X-HubSpot-Request-Timestamp')
uri = request.url
if not verify_hubspot_signature_v3(
'POST', uri, request.get_data(as_text=True),
timestamp, signature,
os.environ['HUBSPOT_CLIENT_SECRET']
):
abort(401)
# Acknowledge immediately, process asynchronously
return 'OK', 200
Respond within the 5-second timeout
HubSpot has a strict 5-second timeout. Your endpoint must acknowledge the webhook quickly and defer any heavy processing.
Handle batched events
A single webhook request can contain up to 100 events. Process each event individually while tracking the batch context.
Implement idempotent processing
HubSpot does not guarantee exactly-once delivery. Use the eventId combined with other event fields to deduplicate:
async function processEvent(event) {
const idempotencyKey = `${event.portalId}-${event.eventId}-${event.attemptNumber}`;
const exists = await redis.get(`processed:${idempotencyKey}`);
if (exists) {
console.log(`Event ${idempotencyKey} already processed, skipping`);
return;
}
await handleEvent(event);
await redis.setex(`processed:${idempotencyKey}`, 86400, '1'); // 24-hour TTL
}
Use occurredAt for ordering
HubSpot does not guarantee that webhook notifications arrive in the order events occurred. Always use the occurredAt timestamp to determine the correct sequence of events:
async function handlePropertyChange(event) {
const { objectId, propertyName, propertyValue, occurredAt } = event;
// Only apply if this is the most recent change
const lastUpdate = await redis.get(`last-update:${objectId}:${propertyName}`);
if (lastUpdate && parseInt(lastUpdate) > occurredAt) {
console.log('Stale event, skipping');
return;
}
await updateRecord(objectId, propertyName, propertyValue);
await redis.set(`last-update:${objectId}:${propertyName}`, occurredAt);
}
Filter by changeSource
HubSpot webhooks fire for all changes, including those made by your own integration. Use the changeSource field to avoid processing loops:
function shouldProcess(event) {
// Skip changes made by our own integration
const ignoredSources = ['INTEGRATION', 'API'];
if (ignoredSources.includes(event.changeSource)) {
return false;
}
return true;
}
HubSpot webhook limitations and pain points
Strict 5-second timeout
The Problem: HubSpot webhooks have a hardcoded 5-second timeout that cannot be configured or extended. If your endpoint doesn't respond within 5 seconds, HubSpot considers the delivery failed and queues it for retry.
Why It Happens: HubSpot optimizes for high throughput across its entire customer base, and a short timeout prevents slow endpoints from creating bottlenecks in their delivery pipeline.
Workarounds:
- Always acknowledge webhooks immediately with a
200response and process events asynchronously using a message queue (e.g., Hookdeck, SQS, RabbitMQ, Redis). - Avoid making any external API calls or database writes before responding.
- If using serverless functions (Lambda, Cloud Functions), be aware of cold start latency, which can consume a significant portion of the 5-second window.
How Hookdeck Can Help: Hookdeck receives webhook deliveries on your behalf and reliably forwards them to your endpoint with configurable timeouts. This decouples HubSpot's strict timeout from your actual processing time, giving your endpoint as much time as it needs without triggering HubSpot's retry logic.
No event ordering guarantee
The Problem: HubSpot explicitly does not guarantee that webhook notifications arrive in the order events occurred. When a contact's property changes multiple times in quick succession, you may receive the notifications out of order, leading to stale data overwriting newer values.
Why It Happens: HubSpot's webhook delivery system is distributed and optimized for throughput rather than strict ordering. Events are batched and dispatched asynchronously, so delivery order depends on network conditions, retry timing, and internal queuing.
Workarounds:
- Always use the
occurredAttimestamp to determine event ordering. - Implement last-write-wins logic by storing and comparing timestamps before applying updates.
- For critical data, consider fetching the current state from HubSpot's API after receiving a webhook rather than relying solely on the payload.
How Hookdeck Can Help: Hookdeck's event ordering capabilities can buffer and deliver webhook events in the correct chronological order based on event metadata, ensuring your endpoint processes events sequentially.
Duplicate event delivery
The Problem: HubSpot acknowledges that duplicate webhook notifications are possible. Additionally, certain actions generate multiple events, for example, creating a primary association between two records will also create a corresponding non-primary association, resulting in two webhook messages. A privacy deletion also triggers a regular deletion event.
Why It Happens: HubSpot uses at-least-once delivery semantics, meaning it prioritizes ensuring events are delivered (even if duplicated) over guaranteeing exactly-once delivery. Additionally, some CRM operations inherently trigger multiple related events.
Workarounds:
- Implement idempotent processing using a combination of
eventId,portalId, andattemptNumberas a deduplication key. - Track processed events in a cache (e.g., Redis) with a TTL that exceeds the 24-hour retry window.
- Be aware that association change events fire for both sides of the relationship.
How Hookdeck Can Help: Hookdeck's deduplication feature can automatically filter duplicate webhook events based on payload content or custom identifiers, ensuring your endpoint only processes each unique event once.
No payload context — only IDs and changed values
The Problem: HubSpot webhook payloads contain only the event metadata (object ID, changed property name, new property value) but not the full record. If you need additional context about a contact, deal, or company, you must make a separate API call to fetch the full record.
Why It Happens: Keeping payloads lightweight allows HubSpot to deliver notifications quickly and reduces the payload size for high-volume accounts. Including full records in every webhook would significantly increase bandwidth and processing overhead.
Workarounds:
- After receiving a webhook, make a follow-up API call to HubSpot to fetch the full record using the
objectId. - Batch API lookups to stay within HubSpot's rate limits (100 requests per 10 seconds for OAuth apps).
- Cache frequently accessed record data to minimize redundant API calls.
How Hookdeck Can Help: Hookdeck's transformation capabilities can enrich webhook payloads in-flight by fetching additional data from HubSpot's API before forwarding events to your endpoint, reducing the complexity of your webhook handler.
Webhook settings cached for up to 5 minutes
The Problem: Changes to webhook URL, concurrency limits, or subscription settings can take up to 5 minutes to propagate. During this window, webhooks may still be sent to a previous URL or with outdated settings, creating confusion during deployment and debugging.
Why It Happens: HubSpot caches webhook configuration for performance reasons. The 5-minute cache window reduces the frequency of configuration lookups across their distributed delivery infrastructure.
Workarounds:
- Plan deployments with the 5-minute propagation delay in mind.
- Keep your old endpoint active for at least 5 minutes after updating the webhook URL.
- Use a stable, permanent webhook URL rather than frequently changing endpoints.
How Hookdeck Can Help: By using Hookdeck as your permanent webhook URL, you can change your downstream endpoint instantly through Hookdeck's dashboard without ever needing to update the URL in HubSpot's settings. This eliminates the 5-minute propagation delay entirely.
Aggressive retry behavior with no control
The Problem: When a webhook delivery fails, HubSpot retries up to 10 times over a 24-hour period. There is no way to configure the retry schedule, disable retries, or manually retry failed deliveries. HubSpot retries on connection failures, timeouts, and any 4xx or 5xx HTTP response.
Why It Happens: HubSpot's retry policy is designed to maximize delivery success, but the one-size-fits-all approach doesn't account for scenarios where retries are undesirable (e.g., when you intentionally reject an event with a 4xx status).
Workarounds:
- Always return a
200status code, even for events you want to ignore, to prevent unnecessary retries. - Handle error cases gracefully within your processing logic rather than rejecting at the HTTP level.
- Monitor the
attemptNumberfield to detect retry storms.
How Hookdeck Can Help: Hookdeck provides fully configurable retry policies with exponential backoff, custom retry intervals, and the ability to manually replay individual failed events. You can also set specific retry rules based on response codes.
Limited delivery visibility and no dead letter queue
The Problem: HubSpot provides minimal visibility into webhook delivery status. There is no dashboard showing delivery success rates, response times, or error logs. Failed webhooks that exhaust all 10 retries are silently dropped with no way to inspect or replay them.
Why It Happens: HubSpot's webhook infrastructure is primarily designed as a notification system rather than a guaranteed delivery pipeline. The developer tooling around observability has historically been limited.
Workarounds:
- Implement your own logging of all received webhook events for gap analysis.
- Build health monitoring for your webhook endpoint and alert when delivery issues occur.
- Use the private app test feature to validate webhook configuration, but be aware test payloads may differ from real events.
How Hookdeck Can Help: Hookdeck's dashboard provides complete visibility into webhook delivery status, latency, errors, and response codes. Failed webhooks are automatically preserved in a dead letter queue, allowing you to inspect, debug, and replay them once issues are resolved.
All-or-nothing subscription model
The Problem: Webhook subscriptions in a public app apply to every HubSpot account that installs your app. You cannot selectively enable or disable webhooks for specific accounts. If you subscribe to contact.creation, every installed account will trigger that webhook.
Why It Happens: HubSpot's webhook system is designed at the app level, not the account level. This simplifies configuration for app developers but removes granularity for multi-tenant integrations.
Workarounds:
- Filter events by
portalIdin your webhook handler to route or ignore events from specific accounts. - Use feature flags or account-level configuration in your backend to control which accounts' events get processed.
- For account-specific needs, consider using private app webhooks instead of public app webhooks.
How Hookdeck Can Help: Hookdeck's filtering and routing rules allow you to create per-account processing logic, routing webhooks from different HubSpot portal IDs to different destinations or applying custom filtering rules without modifying your application code.
Testing HubSpot webhooks
Use the built-in test feature
Both public and private apps include a test function:
- Private apps: Navigate to Settings > Integrations > Private Apps, select your app, go to the Webhooks tab, expand a subscription type, and click Test.
- Public apps: In your developer account, navigate to your app's Webhooks settings, hover over a subscription type, click Details, and click Test.
Be aware that test payloads may differ from real event data.
Use a request inspector
Before building your handler, inspect real HubSpot payloads using a tool like Hookdeck Console:
- Create a temporary webhook URL.
- Configure it as your webhook target URL in HubSpot.
- Trigger a real event (e.g., create a contact in a test HubSpot account).
- Inspect the payload structure, headers, and signature values.
Validate with realistic scenarios
Test your webhook integration with scenarios that mirror production:
- Single record creation and deletion.
- Property changes that trigger multiple events.
- Bulk imports that generate high-volume batched webhooks.
- Association changes between different object types.
- Merge events and privacy deletion events.
- Retry handling by temporarily returning error codes.
Conclusion
HubSpot webhooks provide an event-driven foundation for keeping external systems in sync with CRM data. The comprehensive set of supported event types across contacts, companies, deals, tickets, and more enables sophisticated integrations that react to changes in real time.
However, the strict 5-second timeout, lack of event ordering guarantees, limited payload context, and minimal delivery visibility mean production deployments require careful architectural planning. Implementing signature verification, idempotent processing, async event handling, and proper occurredAt-based ordering will address most common issues.
For integrations with moderate event volumes and well-optimized endpoints, HubSpot's built-in webhook system works well when combined with proper error handling and deduplication. For high-volume multi-tenant integrations, complex routing requirements, or mission-critical sync workflows where delivery guarantees matter, webhook infrastructure like Hookdeck can address HubSpot's limitations by providing configurable timeouts, automatic deduplication, event ordering, payload transformation, and comprehensive delivery monitoring without modifying your HubSpot configuration.