Claude-code-plugins-plus-skills hubspot-webhooks-events
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/hubspot-pack/skills/hubspot-webhooks-events" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-hubspot-webhooks-events && rm -rf "$T"
manifest:
plugins/saas-packs/hubspot-pack/skills/hubspot-webhooks-events/SKILL.mdsource content
HubSpot Webhooks & Events
Overview
Set up HubSpot webhook subscriptions for CRM events (contact/company/deal creation, updates, deletions) with v3 signature verification and idempotent event handling.
Prerequisites
- HubSpot public app (webhooks require a public app, not a private app)
- Client secret from your app settings (for signature verification)
- HTTPS endpoint accessible from the internet
- Optional: Redis or database for idempotency
Instructions
Step 1: Understand HubSpot Webhook Events
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.propertyChangecontact.privacyDeletion
,company.creation
,company.deletioncompany.propertyChange
,deal.creation
,deal.deletiondeal.propertyChange
,ticket.creation
,ticket.deletionticket.propertyChange
,contact.merge
,company.mergedeal.merge
,contact.associationChange
,company.associationChangedeal.associationChange
Step 2: Set Up Webhook Endpoint with Signature Verification
import 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) ); } );
Step 3: Signature Verification Functions
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)); }
Step 4: Event Handler with Idempotency
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}`); } }
Step 5: Register Webhook Subscriptions
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');
Output
- Webhook endpoint with v3 signature verification
- Event handler for contact, company, deal, and ticket events
- Idempotent processing preventing duplicate handling
- Replay protection via timestamp validation
Error Handling
| 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 |
| Previous delivery failed | Normal retry behavior -- process normally |
Examples
Test Webhooks Locally
# 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
Resources
Next Steps
For performance optimization, see
hubspot-performance-tuning.