When a lead fills out a form on your website, you need to know immediately—not after your sync job runs in 15 minutes. HubSpot webhooks solve this problem by sending real-time HTTP notifications to your server the moment CRM events occur, enabling you to sync contacts across platforms, trigger automated workflows, update dashboards instantly, and respond to customer actions in real-time.
Whether you're building custom CRM integrations, automating sales workflows, or synchronizing HubSpot data with external systems, webhooks provide the foundation for event-driven architecture. This guide covers everything you need to implement HubSpot webhooks securely and efficiently.
In this comprehensive guide, you'll learn:
- How to set up HubSpot webhooks in your developer account
- Available event types for contacts, deals, companies, and more
- Three signature verification methods with code examples
- Production-ready implementation patterns
- Testing strategies including our Webhook Payload Generator tool
What Are HubSpot Webhooks?
HubSpot webhooks are HTTP callbacks that notify your application when specific events occur in a HubSpot account. Instead of polling the HubSpot API every few minutes to check for changes, webhooks push data to your server in real-time, typically within seconds of the event occurring.
How HubSpot webhooks work:
[CRM Event in HubSpot] → [HubSpot Webhook Service] → [Your HTTPS Endpoint] → [Your Application Logic]
For example, when a contact is created in HubSpot, the webhook service immediately sends a POST request to your configured endpoint with details about the new contact.
Key benefits of HubSpot webhooks:
- Real-time updates: Receive notifications within seconds of events occurring
- Reduced API calls: No need to poll the API repeatedly, saving quota and reducing latency
- Event-driven architecture: Build reactive systems that respond to CRM changes instantly
- Scalability: HubSpot handles delivery retries and batching automatically
- Cost-effective: Webhook requests don't count against your API rate limits
Prerequisites for HubSpot webhooks:
- A HubSpot developer account (free to create)
- A public app created in your developer account
- A publicly accessible HTTPS endpoint to receive webhooks
- Appropriate OAuth scopes for the events you want to subscribe to (e.g.,
crm.objects.contacts.read)
Setting Up HubSpot Webhooks
Setting up HubSpot webhooks requires creating a public app and configuring webhook subscriptions through the developer portal. Here's the complete step-by-step process:
Step 1: Create a Public App
- Log in to your HubSpot developer account at developers.hubspot.com
- Navigate to Apps in the top menu
- Click Create app and enter your app name and description
- Configure the required OAuth scopes based on the events you want to track
Step 2: Access Webhook Settings
- In your app dashboard, click on your app name
- Select Webhooks from the left sidebar
- You'll see the webhook configuration interface with Target URL and throttling settings
Step 3: Configure Your Webhook Endpoint
- In the Target URL field, enter your HTTPS endpoint:
https://yourdomain.com/webhooks/hubspot - Set the Concurrency limit (minimum 5, maximum 10 concurrent requests per installation)
- Click Save to store your configuration
Important: Configuration changes are cached for up to 5 minutes, so updates may take time to propagate.
Step 4: Create Event Subscriptions
You can create subscriptions via the UI or programmatically using the API:
Via API:
curl -X POST \
'https://api.hubapi.com/webhooks/v3/{appId}/subscriptions?hapikey=YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"eventType": "contact.creation",
"active": true
}'
For property change events, include the property name:
curl -X POST \
'https://api.hubapi.com/webhooks/v3/{appId}/subscriptions?hapikey=YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"eventType": "contact.propertyChange",
"propertyName": "email",
"active": true
}'
Step 5: Activate Subscriptions
New subscriptions start in a paused state. Activate them by:
- Setting
"active": truewhen creating the subscription via API - Or updating the subscription status after creation
Retrieve your webhook secret:
Your app secret is used for signature verification. Find it in your app's Auth settings in the developer portal. Store it securely as an environment variable—never commit it to version control.
Pro Tips:
- Use separate apps for testing and production environments with different webhook URLs
- Start with a small number of event subscriptions and add more as needed
- Configure proper logging before activating subscriptions to debug issues
- HubSpot allows up to 1,000 subscriptions per application
- Consider using the same endpoint for multiple event types and routing based on
eventTypein the payload
HubSpot Webhook Events & Payloads
HubSpot provides webhooks for CRM objects including contacts, companies, deals, tickets, products, line items, and conversations. Each event type delivers specific information about what changed and when.
Available Event Types
| Event Type | Description | Common Use Case |
|---|---|---|
contact.creation | Contact created in HubSpot | Sync new leads to external CRM |
contact.propertyChange | Contact property updated | Track lifecycle stage changes |
contact.deletion | Contact deleted | Remove from external systems |
contact.merge | Contacts merged together | Update records with primary ID |
contact.associationChange | Contact associations modified | Track deal-contact relationships |
deal.creation | Deal created | Trigger sales notifications |
deal.propertyChange | Deal property updated | Monitor deal stage progression |
deal.deletion | Deal deleted | Clean up external records |
company.creation | Company created | Add to account database |
company.propertyChange | Company property updated | Track firmographic changes |
ticket.creation | Support ticket created | Create tickets in external system |
ticket.propertyChange | Ticket property updated | Sync support status |
conversation.creation | New conversation started | Trigger chat notifications |
conversation.newMessage | New message in conversation | Real-time chat alerts |
Detailed Event Payload Examples
Event: contact.creation
Description: Triggered when a new contact is created in HubSpot through any method (form submission, manual entry, API, import).
Payload Structure:
[
{
"objectId": 12345,
"eventId": 1234567890,
"subscriptionId": 98765,
"portalId": 62515,
"occurredAt": 1673980800000,
"subscriptionType": "contact.creation",
"attemptNumber": 0,
"appId": 123456
}
]
Key Fields:
objectId- The unique ID of the newly created contacteventId- Unique identifier for this webhook event (use for idempotency)subscriptionId- ID of the subscription that triggered this notificationportalId- Your HubSpot account IDoccurredAt- Millisecond timestamp when the event occurredattemptNumber- Retry attempt count (0 for first delivery, increments on retries)appId- Your application's ID
Usage: When this event fires, fetch the complete contact details using the HubSpot Contacts API with the objectId.
Event: contact.propertyChange
Description: Triggered when a specific contact property is modified. You must subscribe to individual properties—there is no "any property changed" subscription.
Payload Structure:
[
{
"objectId": 12345,
"propertyName": "lifecyclestage",
"propertyValue": "customer",
"changeSource": "CRM",
"eventId": 1234567891,
"subscriptionId": 98766,
"portalId": 62515,
"occurredAt": 1673981400000,
"subscriptionType": "contact.propertyChange",
"attemptNumber": 0,
"appId": 123456
}
]
Key Fields:
propertyName- The property that changed (e.g., "email", "lifecyclestage", "company")propertyValue- The new value of the propertychangeSource- Where the change originated ("CRM", "WORKFLOWS", "INTEGRATION", etc.)- All standard fields from contact.creation event
Usage: Monitor critical property changes like lifecycle stage transitions, email updates, or custom field modifications without polling.
Event: deal.creation
Description: Triggered when a new deal is created in the HubSpot CRM.
Payload Structure:
[
{
"objectId": 67890,
"eventId": 1234567892,
"subscriptionId": 98767,
"portalId": 62515,
"occurredAt": 1673982000000,
"subscriptionType": "deal.creation",
"attemptNumber": 0,
"appId": 123456
}
]
Usage: Notify sales teams via Slack, create opportunities in external systems, or trigger automated deal workflows.
Event: company.creation
Description: Triggered when a new company is added to HubSpot.
Payload Structure:
[
{
"objectId": 54321,
"eventId": 1234567893,
"subscriptionId": 98768,
"portalId": 62515,
"occurredAt": 1673982600000,
"subscriptionType": "company.creation",
"attemptNumber": 0,
"appId": 123456
}
]
Usage: Sync company records to accounting systems, trigger account-based marketing workflows, or enrich company data via third-party APIs.
Event: contact.merge
Description: Triggered when two or more contacts are merged into a single record.
Payload Structure:
[
{
"primaryObjectId": 12345,
"mergedObjectIds": [67890, 11111],
"numberOfPropertiesMoved": 15,
"eventId": 1234567894,
"subscriptionId": 98769,
"portalId": 62515,
"occurredAt": 1673983200000,
"subscriptionType": "contact.merge",
"attemptNumber": 0,
"appId": 123456
}
]
Key Fields:
primaryObjectId- The surviving contact ID after mergemergedObjectIds- Array of contact IDs that were merged into the primarynumberOfPropertiesMoved- Count of properties transferred during merge
Usage: Update external systems to use the primary contact ID and deprecate merged IDs to maintain data consistency.
Event Batching
HubSpot delivers webhooks in batches of up to 100 events per request. Your endpoint receives a JSON array containing multiple notifications. Each notification represents a separate event with its own eventId for idempotency tracking.
Example batched payload:
[
{
"objectId": 12345,
"eventId": 1000001,
"subscriptionType": "contact.creation",
"occurredAt": 1673980800000
},
{
"objectId": 12346,
"eventId": 1000002,
"subscriptionType": "contact.creation",
"occurredAt": 1673980801000
},
{
"objectId": 12347,
"eventId": 1000003,
"subscriptionType": "contact.propertyChange",
"propertyName": "email",
"propertyValue": "[email protected]",
"occurredAt": 1673980802000
}
]
Process each event in the array individually, using occurredAt timestamps to establish chronological order if needed.
Webhook Signature Verification
HubSpot provides three signature verification methods to ensure webhook requests authentically originate from HubSpot and haven't been tampered with. Always verify signatures to prevent webhook spoofing and replay attacks.
Why Signature Verification Matters
Without signature verification, attackers could:
- Send fake webhook requests to your endpoint
- Replay captured webhook requests (replay attacks)
- Inject malicious data into your application
- Trigger unauthorized actions in your system
HubSpot's signature methods:
- Version 3 (Recommended): HMAC SHA-256 with timestamp validation (prevents replay attacks)
- Version 2: SHA-256 with method and URI validation
- Version 1: SHA-256 with body only (legacy, less secure)
This guide focuses on Version 3 as it provides the strongest security guarantees.
Version 3 Signature Verification (Recommended)
Algorithm: HMAC SHA-256 (base64 encoded)
Header: X-HubSpot-Signature-v3
Additional Header: X-HubSpot-Request-Timestamp (millisecond timestamp)
What's included in the signature:
- HTTP method (e.g., "POST")
- Request URI with specific URL-encoded characters decoded
- Request body (raw JSON)
- Timestamp from header
Step-by-Step Verification:
- Extract headers - Get
X-HubSpot-Signature-v3andX-HubSpot-Request-Timestamp - Validate timestamp - Reject requests older than 5 minutes to prevent replay attacks
- Decode URI - Decode specific URL-encoded characters in the URI (
%3A,%2F,%3F,%40,%21,%24,%27,%28,%29,%2A,%2C,%3B) - Build signature string - Concatenate: method + URI + body + timestamp
- Compute HMAC - Generate HMAC SHA-256 using your app secret
- Base64 encode - Encode the resulting hash
- Compare - Use constant-time comparison to match against the header signature
Code Examples
Node.js / Express
const crypto = require('crypto');
const express = require('express');
const app = express();
// IMPORTANT: Use raw body for signature verification
app.use('/webhooks/hubspot', express.raw({type: 'application/json'}));
app.post('/webhooks/hubspot', (req, res) => {
const signature = req.headers['x-hubspot-signature-v3'];
const timestamp = req.headers['x-hubspot-request-timestamp'];
const secret = process.env.HUBSPOT_APP_SECRET;
// 1. Validate timestamp (5-minute window)
const currentTime = Date.now();
const requestTime = parseInt(timestamp, 10);
const timeDiff = Math.abs(currentTime - requestTime);
if (timeDiff > 5 * 60 * 1000) { // 5 minutes in milliseconds
console.error('Request timestamp too old');
return res.status(401).send('Request expired');
}
// 2. Build signature string
const method = req.method;
const uri = req.originalUrl; // Already decoded by Express
const body = req.body; // Raw buffer
const signatureString = method + uri + body.toString('utf8') + timestamp;
// 3. Compute expected signature
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signatureString, 'utf8')
.digest('base64');
// 4. Verify signature using constant-time comparison
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)) {
console.error('Invalid signature');
return res.status(401).send('Unauthorized');
}
// 5. Parse payload after verification
const payload = JSON.parse(body.toString('utf8'));
// Process webhook events
console.log(`Received ${payload.length} webhook events`);
payload.forEach(event => {
console.log(`Event ${event.eventId}: ${event.subscriptionType}`);
});
// Return 200 immediately
res.status(200).send('Webhook received');
// Process async
processWebhooksAsync(payload);
});
function processWebhooksAsync(events) {
// Queue or process events without blocking response
events.forEach(event => {
// Your business logic here
});
}
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`HubSpot webhook server listening on port ${PORT}`);
});
Python / Flask
import hmac
import hashlib
import base64
import time
from flask import Flask, request
app = Flask(__name__)
HUBSPOT_APP_SECRET = 'your_app_secret_here'
@app.route('/webhooks/hubspot', methods=['POST'])
def hubspot_webhook():
# Get headers
signature = request.headers.get('X-HubSpot-Signature-v3')
timestamp = request.headers.get('X-HubSpot-Request-Timestamp')
if not signature or not timestamp:
return 'Missing signature headers', 401
# 1. Validate timestamp (5-minute window)
current_time = int(time.time() * 1000) # Milliseconds
request_time = int(timestamp)
time_diff = abs(current_time - request_time)
if time_diff > 5 * 60 * 1000: # 5 minutes
return 'Request expired', 401
# 2. Get request data
method = request.method
uri = request.full_path.rstrip('?') # Remove trailing ? if no query params
body = request.get_data()
# 3. Build signature string
signature_string = f"{method}{uri}{body.decode('utf-8')}{timestamp}"
# 4. Compute expected signature
expected_signature = base64.b64encode(
hmac.new(
HUBSPOT_APP_SECRET.encode('utf-8'),
signature_string.encode('utf-8'),
hashlib.sha256
).digest()
).decode('utf-8')
# 5. Verify signature using constant-time comparison
if not hmac.compare_digest(signature, expected_signature):
return 'Unauthorized', 401
# 6. Parse payload
data = request.get_json()
# Process webhook events
print(f"Received {len(data)} webhook events")
for event in data:
print(f"Event {event['eventId']}: {event['subscriptionType']}")
# Return 200 immediately
return 'Webhook received', 200
PHP
<?php
$secret = getenv('HUBSPOT_APP_SECRET');
$signature = $_SERVER['HTTP_X_HUBSPOT_SIGNATURE_V3'];
$timestamp = $_SERVER['HTTP_X_HUBSPOT_REQUEST_TIMESTAMP'];
// 1. Validate timestamp (5-minute window)
$currentTime = round(microtime(true) * 1000);
$requestTime = intval($timestamp);
$timeDiff = abs($currentTime - $requestTime);
if ($timeDiff > 5 * 60 * 1000) {
http_response_code(401);
die('Request expired');
}
// 2. Get request data
$method = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];
$payload = file_get_contents('php://input');
// 3. Build signature string
$signatureString = $method . $uri . $payload . $timestamp;
// 4. Compute expected signature
$expectedSignature = base64_encode(
hash_hmac('sha256', $signatureString, $secret, true)
);
// 5. Verify signature using constant-time comparison
if (!hash_equals($signature, $expectedSignature)) {
http_response_code(401);
die('Unauthorized');
}
// 6. Parse payload
$data = json_decode($payload, true);
// Process webhook events
error_log("Received " . count($data) . " webhook events");
foreach ($data as $event) {
error_log("Event {$event['eventId']}: {$event['subscriptionType']}");
}
// Return 200 immediately
http_response_code(200);
echo 'Webhook received';
?>
Common Verification Errors
-
❌ Parsing JSON before verification: The body is modified, breaking signature validation
- ✅ Solution: Use raw body parser, verify signature first, then parse JSON
-
❌ Using wrong secret: Test vs production app secrets are different
- ✅ Solution: Verify you're using the correct app secret from developer portal
-
❌ Not validating timestamp: Vulnerable to replay attacks
- ✅ Solution: Always check timestamp is within 5-minute window for v3 signatures
-
❌ String comparison instead of constant-time: Vulnerable to timing attacks
- ✅ Solution: Use
crypto.timingSafeEqual()(Node),hmac.compare_digest()(Python), orhash_equals()(PHP)
- ✅ Solution: Use
-
❌ Incorrect signature string construction: Order matters
- ✅ Solution: Follow exact order: method + URI + body + timestamp
Testing HubSpot Webhooks
Testing HubSpot webhooks locally presents challenges since HubSpot's servers can't reach localhost. You need either a publicly accessible URL or a way to generate test payloads locally.
Challenge: HubSpot Can't Reach Localhost
Your local development server at http://localhost:3000 is not accessible from the internet. HubSpot requires an HTTPS endpoint to deliver webhooks, making local testing difficult without additional tools.
Solution 1: ngrok Tunnel
ngrok creates a secure tunnel from a public URL to your localhost, allowing HubSpot to deliver webhooks to your development machine.
Setup ngrok:
# Install ngrok (macOS)
brew install ngrok
# Or download from ngrok.com for other platforms
# Authenticate (sign up for free account first)
ngrok authtoken YOUR_AUTH_TOKEN
# Start your local server
node server.js
# Server running on http://localhost:3000
# In another terminal, start ngrok tunnel
ngrok http 3000
# Output:
# Forwarding https://abc123.ngrok.io -> http://localhost:3000
Configure in HubSpot:
- Copy the ngrok HTTPS URL (e.g.,
https://abc123.ngrok.io) - Go to your HubSpot app webhook settings
- Set Target URL to:
https://abc123.ngrok.io/webhooks/hubspot - Save and test
ngrok Benefits:
- ✅ Receive real webhooks from HubSpot
- ✅ Test full signature verification flow
- ✅ Debug with actual HubSpot payloads
- ✅ Test retry behavior
ngrok Limitations:
- ⚠️ Free tier has session time limits
- ⚠️ URL changes each session (paid plans offer fixed URLs)
- ⚠️ Requires internet connection
- ⚠️ Another service to manage during development
Solution 2: Webhook Payload Generator Tool
For testing signature verification and business logic without external dependencies, use our Webhook Payload Generator to create realistic HubSpot webhook payloads with valid signatures.
How to use:
- Visit our Webhook Payload Generator
- Select "HubSpot" from the provider dropdown
- Choose event type (e.g.,
contact.creation,deal.propertyChange) - Customize payload fields:
- Object ID
- Event ID (for idempotency testing)
- Subscription ID
- Portal ID
- Property name and value (for property change events)
- Enter your app secret for signature generation
- Click Generate Payload
- Copy the generated JSON with valid
X-HubSpot-Signature-v3header - Send to your local endpoint using curl or Postman
Example with curl:
curl -X POST http://localhost:3000/webhooks/hubspot \
-H "Content-Type: application/json" \
-H "X-HubSpot-Signature-v3: GENERATED_SIGNATURE" \
-H "X-HubSpot-Request-Timestamp: 1673980800000" \
-d '[{"objectId": 12345, "eventId": 1000001, "subscriptionType": "contact.creation", "occurredAt": 1673980800000}]'
Webhook Payload Generator Benefits:
- ✅ No tunneling required
- ✅ Test signature verification logic locally
- ✅ Customize payload values for edge cases
- ✅ Test error handling with malformed payloads
- ✅ Generate valid signatures without exposing your app
- ✅ Test batched events (multiple events in array)
- ✅ Validate timestamp expiration handling
Testing Checklist
Before deploying to production, verify:
- Signature verification passes - Test with valid and invalid signatures
- Timestamp validation works - Test with old timestamps (>5 minutes)
- Endpoint returns 200 within 5 seconds - HubSpot timeout threshold
- Idempotency check works - Send same eventId twice, verify single processing
- Error handling for malformed payloads - Test with invalid JSON
- Async processing implemented - Response doesn't block on business logic
- Batch processing works - Test with array of multiple events
- Logging captures all events - Verify you can debug issues
- Event routing works - Different event types handled correctly
Implementation Example
A production-ready HubSpot webhook endpoint must respond quickly, process asynchronously, handle retries gracefully, and implement proper security. Here's a complete working implementation.
Requirements
- Respond within 5 seconds - HubSpot's timeout threshold
- Return 200 status code - Even if processing fails, return 200 to prevent retries
- Process asynchronously - Use queues to avoid blocking the response
- Handle retries gracefully - Check
attemptNumberand implement idempotency - Validate signatures - Always verify authenticity
- Log comprehensively - Track events for debugging and monitoring
Full Node.js Production Example
const express = require('express');
const crypto = require('crypto');
const { Queue } = require('bullmq'); // or any queue library
const Redis = require('ioredis');
const app = express();
// Redis connection for queue and idempotency tracking
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379
});
// Create queue for async webhook processing
const webhookQueue = new Queue('hubspot-webhooks', { connection: redis });
// Configuration
const HUBSPOT_APP_SECRET = process.env.HUBSPOT_APP_SECRET;
const MAX_TIMESTAMP_AGE = 5 * 60 * 1000; // 5 minutes
// Parse raw body for signature verification
app.use('/webhooks/hubspot', express.raw({type: 'application/json'}));
// HubSpot webhook endpoint
app.post('/webhooks/hubspot', async (req, res) => {
const startTime = Date.now();
try {
// 1. Extract headers
const signature = req.headers['x-hubspot-signature-v3'];
const timestamp = req.headers['x-hubspot-request-timestamp'];
if (!signature || !timestamp) {
console.error('Missing signature headers');
return res.status(401).json({ error: 'Missing signature headers' });
}
// 2. Validate timestamp (prevent replay attacks)
const currentTime = Date.now();
const requestTime = parseInt(timestamp, 10);
const timeDiff = Math.abs(currentTime - requestTime);
if (timeDiff > MAX_TIMESTAMP_AGE) {
console.error(`Request timestamp too old: ${timeDiff}ms`);
return res.status(401).json({ error: 'Request expired' });
}
// 3. Build signature string
const method = req.method;
const uri = req.originalUrl;
const body = req.body;
const signatureString = method + uri + body.toString('utf8') + timestamp;
// 4. Compute expected signature
const expectedSignature = crypto
.createHmac('sha256', HUBSPOT_APP_SECRET)
.update(signatureString, 'utf8')
.digest('base64');
// 5. Verify signature using constant-time comparison
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)) {
console.error('Invalid signature');
return res.status(401).json({ error: 'Invalid signature' });
}
// 6. Parse payload after verification
const events = JSON.parse(body.toString('utf8'));
// 7. Validate payload structure
if (!Array.isArray(events) || events.length === 0) {
console.error('Invalid payload structure');
return res.status(200).json({ received: true, error: 'Invalid payload' });
}
// 8. Process each event
const queuedEvents = [];
const duplicateEvents = [];
for (const event of events) {
const eventId = event.eventId;
const eventType = event.subscriptionType;
// Check for duplicate (idempotency)
const exists = await checkIfProcessed(eventId);
if (exists) {
console.log(`Event ${eventId} already processed, skipping`);
duplicateEvents.push(eventId);
continue;
}
// Queue for async processing
await webhookQueue.add('process-event', {
eventId,
eventType,
event,
receivedAt: currentTime
}, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000
}
});
queuedEvents.push(eventId);
}
// 9. Return 200 immediately
const processingTime = Date.now() - startTime;
res.status(200).json({
received: true,
count: events.length,
queued: queuedEvents.length,
duplicates: duplicateEvents.length,
processingTime
});
// Logging
console.log(`Processed ${events.length} events in ${processingTime}ms`);
console.log(`Queued: ${queuedEvents.length}, Duplicates: ${duplicateEvents.length}`);
} catch (error) {
console.error('Webhook processing error:', error);
// Still return 200 to prevent HubSpot retries for our errors
res.status(200).json({ received: true, error: true });
}
});
// Process webhooks from queue
const worker = new Worker('hubspot-webhooks', async (job) => {
const { eventId, eventType, event, receivedAt } = job.data;
try {
// Mark as processing
await markAsProcessing(eventId);
// Handle different event types
switch (eventType) {
case 'contact.creation':
await handleContactCreation(event);
break;
case 'contact.propertyChange':
await handleContactPropertyChange(event);
break;
case 'deal.creation':
await handleDealCreation(event);
break;
case 'company.creation':
await handleCompanyCreation(event);
break;
case 'contact.merge':
await handleContactMerge(event);
break;
default:
console.warn(`Unknown event type: ${eventType}`);
}
// Mark as completed
await markAsCompleted(eventId);
console.log(`Successfully processed event ${eventId}`);
} catch (error) {
console.error(`Failed to process event ${eventId}:`, error);
await markAsFailed(eventId, error.message);
throw error; // Will trigger queue retry
}
}, { connection: redis });
// Business logic handlers
async function handleContactCreation(event) {
console.log(`New contact created: ${event.objectId}`);
// Fetch full contact details from HubSpot API
const contact = await fetchContactFromHubSpot(event.objectId);
// Sync to your database
await db.contacts.create({
hubspotId: event.objectId,
email: contact.properties.email,
firstName: contact.properties.firstname,
lastName: contact.properties.lastname,
createdAt: new Date(event.occurredAt)
});
// Trigger welcome email workflow
await sendWelcomeEmail(contact.properties.email);
}
async function handleContactPropertyChange(event) {
console.log(`Contact ${event.objectId} property changed: ${event.propertyName} = ${event.propertyValue}`);
// Update your database
await db.contacts.update({
where: { hubspotId: event.objectId },
data: { [event.propertyName]: event.propertyValue }
});
// Handle specific property changes
if (event.propertyName === 'lifecyclestage' && event.propertyValue === 'customer') {
await notifySalesTeam(event.objectId);
}
}
async function handleDealCreation(event) {
console.log(`New deal created: ${event.objectId}`);
// Fetch deal details
const deal = await fetchDealFromHubSpot(event.objectId);
// Create opportunity in external CRM
await externalCRM.createOpportunity({
hubspotDealId: event.objectId,
amount: deal.properties.amount,
stage: deal.properties.dealstage,
closeDate: deal.properties.closedate
});
// Notify sales team via Slack
await notifySlack(`New deal created: $${deal.properties.amount}`);
}
async function handleCompanyCreation(event) {
console.log(`New company created: ${event.objectId}`);
// Fetch company details
const company = await fetchCompanyFromHubSpot(event.objectId);
// Enrich company data via third-party API
const enrichedData = await enrichCompanyData(company.properties.domain);
// Update HubSpot with enriched data
await updateHubSpotCompany(event.objectId, enrichedData);
}
async function handleContactMerge(event) {
console.log(`Contacts merged: ${event.mergedObjectIds} -> ${event.primaryObjectId}`);
// Update all references to merged IDs
await db.contacts.updateMany({
where: { hubspotId: { in: event.mergedObjectIds } },
data: {
hubspotId: event.primaryObjectId,
mergedAt: new Date()
}
});
}
// Helper functions
async function checkIfProcessed(eventId) {
const key = `webhook:processed:${eventId}`;
return await redis.exists(key);
}
async function markAsProcessing(eventId) {
const key = `webhook:processed:${eventId}`;
await redis.set(key, 'processing', 'EX', 86400); // 24 hour TTL
await db.webhookEvents.create({
data: {
eventId,
status: 'processing',
createdAt: new Date()
}
});
}
async function markAsCompleted(eventId) {
const key = `webhook:processed:${eventId}`;
await redis.set(key, 'completed', 'EX', 86400);
await db.webhookEvents.update({
where: { eventId },
data: {
status: 'completed',
completedAt: new Date()
}
});
}
async function markAsFailed(eventId, error) {
const key = `webhook:processed:${eventId}`;
await redis.set(key, `failed:${error}`, 'EX', 86400);
await db.webhookEvents.update({
where: { eventId },
data: {
status: 'failed',
error,
failedAt: new Date()
}
});
}
// Mock external API functions (implement based on your needs)
async function fetchContactFromHubSpot(contactId) {
// Implement using @hubspot/api-client
return { properties: {} };
}
async function fetchDealFromHubSpot(dealId) {
return { properties: {} };
}
async function fetchCompanyFromHubSpot(companyId) {
return { properties: {} };
}
async function sendWelcomeEmail(email) {
// Implement email sending logic
}
async function notifySalesTeam(contactId) {
// Implement notification logic
}
async function notifySlack(message) {
// Implement Slack webhook
}
async function enrichCompanyData(domain) {
// Implement company enrichment
return {};
}
async function updateHubSpotCompany(companyId, data) {
// Implement HubSpot API update
}
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`HubSpot webhook server listening on port ${PORT}`);
});
Key Implementation Details
- Raw body parsing - Required for signature verification before JSON parsing
- Timing-safe comparison - Prevents timing attacks on signature validation
- Timestamp validation - Prevents replay attacks with 5-minute window
- Idempotency check - Redis-based tracking prevents duplicate processing
- Queue-based processing - BullMQ handles async processing with retries
- Error handling - Graceful failures still return 200 to prevent unnecessary retries
- Comprehensive logging - Detailed logs for debugging and monitoring
- Event batching support - Processes arrays of events efficiently
- Retry logic - Queue retries with exponential backoff for transient failures
- Health checks - Monitor endpoint availability
This implementation provides production-grade reliability, security, and observability for HubSpot webhook processing.
Best Practices
Follow these best practices to build secure, reliable, and performant HubSpot webhook integrations.
Security
- ✅ Always verify signatures - Use version 3 with timestamp validation for maximum security
- ✅ Use HTTPS endpoints only - HubSpot requires HTTPS for webhook delivery
- ✅ Store secrets in environment variables - Never commit app secrets to version control
- ✅ Validate timestamp - Reject requests older than 5 minutes to prevent replay attacks
- ✅ Rate limit webhook endpoints - Protect against malicious flooding or misconfigured apps
- ✅ Use constant-time comparison - Prevent timing attacks with
timingSafeEqual()or equivalent - ✅ Sanitize data before use - Don't trust webhook data implicitly, validate before processing
- ✅ Rotate secrets periodically - Update app secrets regularly and update endpoints
Performance
- ✅ Respond within 5 seconds - HubSpot times out after 5 seconds
- ✅ Return 200 immediately - Queue processing, don't block on business logic
- ✅ Use queue systems - Redis with BullMQ, AWS SQS, or RabbitMQ for async processing
- ✅ Implement exponential backoff - For external API calls during processing
- ✅ Monitor processing times - Alert if webhook processing queue gets too deep
- ✅ Process batched events efficiently - Handle arrays of events without overwhelming systems
- ✅ Set concurrency limits appropriately - Start with 5-7 concurrent requests, tune based on load
Reliability
- ✅ Implement idempotency - Track
eventIdto prevent duplicate processing - ✅ Handle duplicate webhooks gracefully - HubSpot may retry on timeouts
- ✅ Implement retry logic - For failed processing, not for webhook receipt
- ✅ Don't rely solely on webhooks - Run periodic reconciliation jobs for data consistency
- ✅ Log all webhook events - Essential for debugging and auditing
- ✅ Store failed events - Persist failures for manual review and reprocessing
- ✅ Monitor retry attempts - Check
attemptNumberfield to detect delivery issues
Monitoring
- ✅ Track webhook delivery success rate - Alert if delivery failures spike
- ✅ Alert on signature verification failures - May indicate security issues or configuration problems
- ✅ Monitor processing queue depth - Indicates system health and capacity
- ✅ Log eventIds for traceability - Essential for debugging and support
- ✅ Set up health checks - Monitor endpoint availability
- ✅ Track event type distribution - Understand which events fire most frequently
- ✅ Monitor HubSpot API quota - Fetching full objects after webhooks consumes API calls
HubSpot-Specific Best Practices
- Subscribe to specific properties only - For
propertyChangeevents, specify property names to reduce noise - Use separate apps for environments - Test and production with different webhook URLs and secrets
- Start with fewer subscriptions - Add more event types gradually as you verify stability
- Fetch full object details - Webhooks contain minimal data; use HubSpot API to fetch complete objects
- Handle event ordering carefully - HubSpot doesn't guarantee delivery order; use
occurredAttimestamps - Configure concurrency appropriately - Minimum 5, maximum 10 concurrent requests per installation
- Monitor subscription health - Check HubSpot dashboard for failed deliveries
- Use proper OAuth scopes - Ensure your app has required scopes for subscribed events
Common Issues & Troubleshooting
Issue 1: Signature Verification Failing
Symptoms:
- 401 errors in HubSpot webhook logs
- "Invalid signature" errors in your application logs
- Webhooks marked as failed in developer portal
Causes & Solutions:
-
❌ Using wrong app secret: Test vs production apps have different secrets
- ✅ Solution: Verify you're using the correct app secret from your app's Auth settings in the developer portal
-
❌ Parsing JSON before verification: Body is modified, breaking signature
- ✅ Solution: Use raw body parser (
express.raw()) and verify signature before parsing JSON
- ✅ Solution: Use raw body parser (
-
❌ Incorrect signature version: Using v1/v2 verification for v3 signatures
- ✅ Solution: Check which header is present and use corresponding verification method
-
❌ Wrong signature string construction: Incorrect order of elements
- ✅ Solution: For v3, use exactly: method + URI + body + timestamp
-
❌ Encoding issues: Inconsistent character encoding (UTF-8 vs ASCII)
- ✅ Solution: Ensure all strings are UTF-8 encoded consistently
-
❌ URI decoding problems: Not decoding required URL-encoded characters
- ✅ Solution: Decode specific characters for v3 signatures as documented
Issue 2: Webhook Timeouts
Symptoms:
- HubSpot shows delivery failures after 5+ seconds
- High
attemptNumbervalues in webhook payloads - Timeout errors in logs
Causes & Solutions:
-
❌ Slow database queries: Blocking response while querying database
- ✅ Solution: Move all database operations to async queue processing
-
❌ External API calls: Waiting for HubSpot API or third-party services
- ✅ Solution: Return 200 immediately, fetch data asynchronously
-
❌ Complex business logic: Heavy processing blocking response
- ✅ Solution: Use background jobs with queue system (BullMQ, AWS SQS)
-
❌ Synchronous event processing: Processing entire batch before responding
- ✅ Solution: Queue events immediately and respond within 1-2 seconds
Issue 3: Duplicate Events
Symptoms:
- Same
eventIdprocessed multiple times - Data inconsistencies from double-processing
- Duplicate notifications or actions
Causes & Solutions:
-
❌ No idempotency check: Processing all events without checking duplicates
- ✅ Solution: Store
eventIdin Redis/database before processing, check existence first
- ✅ Solution: Store
-
❌ Network retries: HubSpot retries on timeout, endpoint processes both
- ✅ Solution: Implement idempotent operations that can safely run multiple times
-
❌ Race conditions: Multiple workers processing same event
- ✅ Solution: Use distributed locks or unique constraints in database
Issue 4: Missing Webhooks
Symptoms:
- Expected webhooks not arriving
- Delivery logs show success but endpoint not receiving
- Events happening in HubSpot but no notifications
Causes & Solutions:
-
❌ Firewall blocking: Server firewall blocking HubSpot's IP addresses
- ✅ Solution: Check firewall rules, ensure port 443 open for HTTPS
-
❌ Wrong webhook URL: Typo in target URL configuration
- ✅ Solution: Verify URL in app webhook settings, test with curl
-
❌ SSL certificate issues: Invalid, expired, or self-signed certificate
- ✅ Solution: Use valid SSL certificate from trusted CA (Let's Encrypt, etc.)
-
❌ Subscription not active: Subscription created but never activated
- ✅ Solution: Check subscription status via API or dashboard, set
active: true
- ✅ Solution: Check subscription status via API or dashboard, set
-
❌ Missing OAuth scopes: App lacks required scopes for event type
- ✅ Solution: Verify required scopes (e.g.,
crm.objects.contacts.read) are granted
- ✅ Solution: Verify required scopes (e.g.,
-
❌ Rate limiting: Your server rate-limiting HubSpot's requests
- ✅ Solution: Whitelist HubSpot, increase rate limits, or remove rate limiting for webhook endpoint
Issue 5: Event Batching Confusion
Symptoms:
- Only processing first event in webhook
- Unexpected array structure in payload
- Missing events from batch
Causes & Solutions:
-
❌ Not handling arrays: Code expects single object, receives array
- ✅ Solution: Always treat payload as array, iterate through events
-
❌ Processing breaks on first error: Error in one event stops batch
- ✅ Solution: Use try-catch inside loop to process all events even if one fails
Debugging Checklist
Use this checklist when troubleshooting webhook issues:
- Check HubSpot webhook delivery logs in developer portal
- Verify webhook endpoint is publicly accessible (test with curl)
- Test signature verification with our Webhook Payload Generator
- Check application logs for errors and timing information
- Verify SSL certificate is valid and not expired
- Confirm subscription is active in HubSpot dashboard
- Check required OAuth scopes are granted to app
- Verify endpoint returns 200 within 5 seconds
- Test idempotency by sending same event twice
- Monitor queue depth if using async processing
- Check for rate limiting or quota errors
- Verify correct app secret is configured
Frequently Asked Questions
Q: How often does HubSpot send webhooks?
A: HubSpot sends webhooks immediately when events occur, typically within seconds. Webhooks are batched with up to 100 events per request for efficiency. If delivery fails, HubSpot retries up to 10 times over 24 hours using exponential backoff.
Q: Can I receive webhooks for past events?
A: No, HubSpot webhooks only send notifications for events that occur after you create the subscription. To process historical data, use the HubSpot CRM API to fetch records and their change history. Consider using the hs_lastmodifieddate property to identify recently changed records.
Q: What happens if my endpoint is down?
A: HubSpot will retry failed deliveries up to 10 times over 24 hours. After all retries are exhausted, the webhook is marked as failed in the delivery logs. You can check logs in your app's webhook dashboard. Implement reconciliation jobs to catch any missed events during downtime.
Q: Do I need different endpoints for test and production?
A: Yes, strongly recommended. Create separate public apps in HubSpot for testing and production environments, each with their own webhook URLs and app secrets. This prevents test events from affecting production data and allows safe testing of changes.
Q: How do I handle webhook ordering?
A: HubSpot does not guarantee delivery order. Events may arrive out of sequence, especially in batches. Best practice: use the occurredAt timestamp field to establish chronological order and handle events idempotently regardless of delivery sequence.
Q: Can I filter which events I receive?
A: Yes, when creating subscriptions, you specify exact event types. For propertyChange events, you can also specify individual property names to only receive notifications when that specific property changes. Only subscribe to events you actually need to reduce processing overhead.
Q: Do webhook requests count against API rate limits?
A: No, incoming webhook POST requests from HubSpot do not count against your API rate limits. However, if your webhook handler makes subsequent API calls to fetch full object details, those calls do count against your limits.
Q: How long are webhook events retained in HubSpot logs?
A: HubSpot retains webhook delivery logs in the developer portal for debugging purposes. However, specific retention periods are not documented. Implement your own logging and storage for audit trails and long-term analysis.
Q: Can I replay failed webhook events?
A: HubSpot does not provide a manual replay feature in the dashboard. If events fail after all retries, you must use the HubSpot API to fetch the relevant data. This is why implementing reconciliation jobs is important for critical integrations.
Q: What's the maximum number of webhook subscriptions?
A: You can create up to 1,000 subscriptions per application. This limit includes all event types and property-specific subscriptions. If you need more granular filtering, implement logic in your webhook handler rather than creating excessive subscriptions.
Next Steps & Resources
Ready to implement HubSpot webhooks in your application? Here's how to get started:
Try It Yourself
- Create a HubSpot public app in your developer account
- Set up a webhook endpoint following the implementation example in this guide
- Test locally with our Webhook Payload Generator to verify signature verification
- Deploy to production with proper monitoring and error handling
- Monitor webhook health using HubSpot's delivery logs and your application metrics
Additional Resources
- HubSpot Official Webhooks API Documentation - Complete API reference
- HubSpot API Client Libraries - Official SDKs for Node.js, Python, PHP, and Ruby
- HubSpot Developer Blog: Implementing Webhooks - Best practices from HubSpot
- Our Complete Webhooks Guide - Webhook fundamentals
- Webhook Signature Verification Guide - Deep dive into signature security
Related Guides
- Stripe Webhooks Guide - Compare webhook implementations across providers
- Testing Webhooks Locally with ngrok - Local development setup
- Webhook Security Best Practices - Comprehensive security guide
Development Tools
- Webhook Payload Generator - Test HubSpot webhooks with valid signatures
- ngrok - Expose localhost for webhook testing
- @hubspot/api-client - Official Node.js SDK
Community & Support
- HubSpot Developer Community - Ask questions and share experiences
- HubSpot Developer Changelog - Stay updated on API changes
- HubSpot Status Page - Check service availability
Need Help?
- Use our Webhook Payload Generator to test your implementation
- Review HubSpot's delivery logs in your app's webhook dashboard
- Check our troubleshooting section above for common issues
- Join the HubSpot developer community for support
Conclusion
HubSpot webhooks provide a powerful, real-time way to integrate CRM data with your applications. By receiving instant notifications for contact creations, deal updates, company changes, and property modifications, you can build responsive, event-driven systems that keep data synchronized across platforms without constant API polling.
By following this guide, you now know how to:
- ✅ Set up HubSpot webhooks in your developer account
- ✅ Verify webhook signatures securely using version 3 with timestamp validation
- ✅ Implement a production-ready webhook endpoint with queue-based processing
- ✅ Handle common issues like duplicates, timeouts, and signature failures
- ✅ Test webhooks effectively using ngrok or our Webhook Payload Generator tool
Remember the key principles for production webhooks:
- Always verify signatures - Use version 3 with HMAC SHA-256 and timestamp validation
- Respond quickly - Return 200 within 5 seconds, process asynchronously
- Process asynchronously - Use queues (BullMQ, AWS SQS) for reliability
- Implement idempotency - Track
eventIdto handle duplicates gracefully - Monitor and log - Track delivery success, processing times, and failures
Start building with HubSpot webhooks today and use our Webhook Payload Generator to test your integration with realistic payloads and valid signatures.
Have questions or run into issues? Check the troubleshooting section above or contact us for assistance with your HubSpot webhook integration.