From hubspot-pack
Implements HubSpot webhook endpoints for CRM events like contact/deal changes, with v3 signature verification and idempotent handling in Express.
How this skill is triggered — by the user, by Claude, or both
Slash command
/hubspot-pack:hubspot-webhooks-eventsThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Set up HubSpot webhook subscriptions for CRM events (contact/company/deal creation, updates, deletions) with v3 signature verification and idempotent event handling.
Set up HubSpot webhook subscriptions for CRM events (contact/company/deal creation, updates, deletions) with v3 signature verification and idempotent event handling.
HubSpot sends webhook events as batches of CRM change notifications:
[
{
"eventId": 100,
"subscriptionId": 1234,
"portalId": 12345678,
"appId": 98765,
"occurredAt": 1711234567890,
"subscriptionType": "contact.propertyChange",
"attemptNumber": 0,
"objectId": 123,
"propertyName": "lifecyclestage",
"propertyValue": "marketingqualifiedlead",
"changeSource": "CRM",
"sourceId": "userId:12345"
}
]
Available subscription types:
contact.creation, contact.deletion, contact.propertyChange, contact.privacyDeletioncompany.creation, company.deletion, company.propertyChangedeal.creation, deal.deletion, deal.propertyChangeticket.creation, ticket.deletion, ticket.propertyChangecontact.merge, company.merge, deal.mergecontact.associationChange, company.associationChange, deal.associationChangeimport express from 'express';
import crypto from 'crypto';
const app = express();
// IMPORTANT: Use raw body for signature verification
app.post('/webhooks/hubspot',
express.raw({ type: 'application/json' }),
async (req, res) => {
// Verify signature (v3)
const signature = req.headers['x-hubspot-signature-v3'] as string;
const timestamp = req.headers['x-hubspot-request-timestamp'] as string;
if (!signature || !timestamp) {
// Fall back to v2 signature
const sigV2 = req.headers['x-hubspot-signature'] as string;
if (!verifySignatureV2(req.body.toString(), sigV2)) {
return res.status(401).json({ error: 'Invalid signature' });
}
} else {
const requestUri = `https://${req.headers.host}${req.originalUrl}`;
if (!verifySignatureV3(req.body.toString(), signature, timestamp, requestUri)) {
return res.status(401).json({ error: 'Invalid signature' });
}
}
// HubSpot sends events as an array
const events: HubSpotWebhookEvent[] = JSON.parse(req.body.toString());
// Respond immediately (HubSpot expects < 5 second response)
res.status(200).json({ received: true });
// Process events asynchronously
processEvents(events).catch(err =>
console.error('Event processing failed:', err)
);
}
);
const CLIENT_SECRET = process.env.HUBSPOT_CLIENT_SECRET!;
// v3 signature (preferred)
function verifySignatureV3(
body: string, signature: string, timestamp: string, requestUri: string
): boolean {
// Reject timestamps older than 5 minutes
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) return false;
const sourceString = `POST${requestUri}${body}${timestamp}`;
const expected = crypto
.createHmac('sha256', CLIENT_SECRET)
.update(sourceString)
.digest('base64');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
// v2 signature (fallback)
function verifySignatureV2(body: string, signature: string): boolean {
const sourceString = CLIENT_SECRET + body;
const expected = crypto
.createHash('sha256')
.update(sourceString)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
interface HubSpotWebhookEvent {
eventId: number;
subscriptionId: number;
portalId: number;
appId: number;
occurredAt: number;
subscriptionType: string;
attemptNumber: number;
objectId: number;
propertyName?: string;
propertyValue?: string;
changeSource?: string;
}
// Track processed events to prevent duplicates
const processedEvents = new Set<number>();
async function processEvents(events: HubSpotWebhookEvent[]): Promise<void> {
for (const event of events) {
// Idempotency: skip already-processed events
if (processedEvents.has(event.eventId)) {
console.log(`Skipping duplicate event: ${event.eventId}`);
continue;
}
try {
await handleEvent(event);
processedEvents.add(event.eventId);
// Clean up old event IDs (keep last 10,000)
if (processedEvents.size > 10000) {
const oldest = [...processedEvents].slice(0, 5000);
oldest.forEach(id => processedEvents.delete(id));
}
} catch (error) {
console.error(`Failed to process event ${event.eventId}:`, error);
}
}
}
async function handleEvent(event: HubSpotWebhookEvent): Promise<void> {
const { subscriptionType, objectId, propertyName, propertyValue } = event;
switch (subscriptionType) {
case 'contact.creation':
console.log(`New contact created: ${objectId}`);
// Sync to your database, send welcome email, etc.
break;
case 'contact.propertyChange':
console.log(`Contact ${objectId}: ${propertyName} = ${propertyValue}`);
if (propertyName === 'lifecyclestage' && propertyValue === 'customer') {
// Trigger onboarding workflow
}
break;
case 'deal.propertyChange':
if (propertyName === 'dealstage') {
console.log(`Deal ${objectId} moved to stage: ${propertyValue}`);
// Notify sales team, update dashboard, etc.
}
break;
case 'deal.creation':
console.log(`New deal created: ${objectId}`);
break;
case 'contact.deletion':
case 'contact.privacyDeletion':
console.log(`Contact ${objectId} deleted`);
// Remove from your systems (GDPR compliance)
break;
default:
console.log(`Unhandled event: ${subscriptionType} for object ${objectId}`);
}
}
Subscriptions are configured in your HubSpot public app settings, or via API:
// Create webhook subscription via API
async function createSubscription(
appId: number,
subscriptionType: string,
propertyName?: string
) {
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_DEVELOPER_API_KEY!,
});
await client.apiRequest({
method: 'POST',
path: `/webhooks/v3/${appId}/subscriptions`,
body: {
eventType: subscriptionType,
propertyName: propertyName || undefined,
active: true,
},
});
}
// Example: Subscribe to lifecycle stage changes
await createSubscription(appId, 'contact.propertyChange', 'lifecyclestage');
await createSubscription(appId, 'deal.creation');
await createSubscription(appId, 'deal.propertyChange', 'dealstage');
| Issue | Cause | Solution |
|---|---|---|
| Invalid signature | Wrong client secret | Verify in App Settings > Auth |
| Duplicate events | HubSpot retries | Implement event ID tracking |
| Timeout (no 200 response) | Slow processing | Respond immediately, process async |
| Missing events | Subscription inactive | Check subscription status in app settings |
attemptNumber > 0 | Previous delivery failed | Normal retry behavior -- process normally |
# Use ngrok to expose local server
ngrok http 3000
# Update webhook URL in HubSpot app settings:
# https://xxxx.ngrok.io/webhooks/hubspot
# Trigger a test: create a contact in HubSpot UI
# Watch your local logs for the webhook event
For performance optimization, see hubspot-performance-tuning.
npx claudepluginhub luxdevnet/claude-plus-lux --plugin hubspot-pack4plugins reuse this skill
First indexed Jul 10, 2026
Builds and hardens HubSpot v3 webhook handlers with HMAC-SHA256 verification, Redis deduplication, async batch processing, dead-letter queuing, and event-ordering guards.
Provides patterns for HubSpot CRM integration including OAuth 2.0 authentication, private app tokens, CRM objects, associations, batch operations, webhooks, and custom objects using Node.js and Python SDKs.
Provides expert patterns for HubSpot CRM integration including OAuth, CRM objects, batch operations, webhooks, and custom objects using Node.js and Python SDKs.