Back to Blog
·Hook Mesh Team

HubSpot Webhooks: CRM Integration Patterns

Learn how to build reliable HubSpot webhook integrations for your CRM workflows. Complete guide covering event subscriptions, signature verification, batch processing, and real-world use cases for lead sync and sales automation.

HubSpot Webhooks: CRM Integration Patterns

HubSpot Webhooks: CRM Integration Patterns

HubSpot webhooks enable real-time data sync between CRM and external systems for lead routing, contact warehousing, and marketing automation. This guide covers setup through advanced batch processing.

Understanding HubSpot Webhook Subscriptions

HubSpot uses a subscription-based model tied to your app. Webhooks are created via dashboard or API.

Event Types

Contact Events

  • contact.creation - New contact added to CRM
  • contact.deletion - Contact removed
  • contact.propertyChange - Any contact property updated
  • contact.privacyDeletion - GDPR deletion request

Company Events

  • company.creation - New company record
  • company.deletion - Company removed
  • company.propertyChange - Company property updated

Deal Events

  • deal.creation - New deal created
  • deal.deletion - Deal removed
  • deal.propertyChange - Deal property changed (stages, amounts, etc.)

Conversation Events

  • conversation.creation - New conversation started
  • conversation.newMessage - Message added to conversation

Setting Up Webhook Subscriptions

Via the HubSpot Developer Portal

  1. Navigate to your app in the HubSpot Developer Portal
  2. Select "Webhooks" from the left sidebar
  3. Configure your target URL endpoint
  4. Select the subscription types you need
  5. Save and activate the subscriptions

Programmatic Setup via API

For more control, create subscriptions programmatically:

const axios = require('axios');

async function createWebhookSubscription(appId, accessToken) {
  const subscriptions = [
    {
      subscriptionType: 'contact.creation',
      propertyName: null,
      active: true
    },
    {
      subscriptionType: 'contact.propertyChange',
      propertyName: 'email',
      active: true
    },
    {
      subscriptionType: 'deal.propertyChange',
      propertyName: 'dealstage',
      active: true
    }
  ];

  for (const subscription of subscriptions) {
    await axios.post(
      `https://api.hubapi.com/webhooks/v3/${appId}/subscriptions`,
      subscription,
      {
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json'
        }
      }
    );
  }
}

propertyChange events require specifying which property to monitor—this keeps volume manageable.

Handling HubSpot Webhook Events

HubSpot sends batched arrays, even for single events. Your endpoint must handle batch format. See receiving and verifying webhooks in Node.js.

Basic Event Handler

const express = require('express');
const crypto = require('crypto');

const app = express();
app.use(express.json());

const CLIENT_SECRET = process.env.HUBSPOT_CLIENT_SECRET;

app.post('/webhooks/hubspot', async (req, res) => {
  // HubSpot expects a response within 5 seconds
  res.status(200).send('OK');

  // Process events asynchronously
  processHubSpotEvents(req.body).catch(err => {
    console.error('Event processing failed:', err);
  });
});

async function processHubSpotEvents(events) {
  for (const event of events) {
    switch (event.subscriptionType) {
      case 'contact.creation':
        await handleNewContact(event);
        break;
      case 'contact.propertyChange':
        await handleContactUpdate(event);
        break;
      case 'deal.propertyChange':
        await handleDealChange(event);
        break;
      default:
        console.log('Unhandled event type:', event.subscriptionType);
    }
  }
}

Signature Verification

Always verify HubSpot signatures in production. This is critical webhook security best practice.

function verifyHubSpotSignature(req) {
  const signature = req.headers['x-hubspot-signature-v3'];
  const timestamp = req.headers['x-hubspot-request-timestamp'];

  // Reject requests older than 5 minutes
  const currentTime = Date.now();
  if (currentTime - parseInt(timestamp) > 300000) {
    throw new Error('Request timestamp too old');
  }

  // Build the signature base string
  const requestBody = JSON.stringify(req.body);
  const uri = `https://${req.headers.host}${req.originalUrl}`;
  const signatureBaseString = `${req.method}${uri}${requestBody}${timestamp}`;

  // Calculate expected signature
  const expectedSignature = crypto
    .createHmac('sha256', CLIENT_SECRET)
    .update(signatureBaseString)
    .digest('base64');

  if (signature !== expectedSignature) {
    throw new Error('Invalid signature');
  }

  return true;
}

// Middleware for signature verification
function hubspotSignatureMiddleware(req, res, next) {
  try {
    verifyHubSpotSignature(req);
    next();
  } catch (error) {
    console.error('Signature verification failed:', error.message);
    res.status(401).send('Unauthorized');
  }
}

app.post('/webhooks/hubspot', hubspotSignatureMiddleware, async (req, res) => {
  // Handler code here
});

Real-World Use Cases

Lead Sync to External Systems

Auto-sync new contacts to billing systems, support desks, or custom apps:

async function handleNewContact(event) {
  const contactId = event.objectId;

  // Fetch full contact details from HubSpot API
  const contact = await hubspotClient.crm.contacts.basicApi.getById(
    contactId,
    ['email', 'firstname', 'lastname', 'company', 'phone']
  );

  // Sync to your external system
  await externalCRM.createLead({
    email: contact.properties.email,
    name: `${contact.properties.firstname} ${contact.properties.lastname}`,
    company: contact.properties.company,
    source: 'hubspot',
    hubspotId: contactId
  });
}

Deal Stage Change Notifications

Trigger actions when deals move through pipeline:

async function handleDealChange(event) {
  if (event.propertyName !== 'dealstage') return;

  const dealId = event.objectId;
  const newStage = event.propertyValue;

  // Fetch deal details
  const deal = await hubspotClient.crm.deals.basicApi.getById(
    dealId,
    ['dealname', 'amount', 'hubspot_owner_id'],
    undefined,
    ['contacts']
  );

  // Stage-specific actions
  switch (newStage) {
    case 'qualifiedtobuy':
      await notifySalesTeam(deal);
      break;
    case 'closedwon':
      await triggerOnboarding(deal);
      await updateRevenueMetrics(deal);
      break;
    case 'closedlost':
      await logLostDealAnalytics(deal);
      break;
  }
}

Marketing Automation Triggers

Trigger marketing workflows on contact property changes:

async function handleContactUpdate(event) {
  const contactId = event.objectId;
  const propertyName = event.propertyName;
  const newValue = event.propertyValue;

  // Trigger campaigns based on lifecycle stage changes
  if (propertyName === 'lifecyclestage' && newValue === 'customer') {
    await marketingPlatform.enrollInCampaign(contactId, 'customer-onboarding');
  }

  // Update email preferences in external ESP
  if (propertyName === 'email') {
    await emailServiceProvider.updateSubscriber({
      oldEmail: event.propertyValueBefore,
      newEmail: newValue,
      hubspotId: contactId
    });
  }
}

Batch Processing Best Practices

HubSpot batches events for efficiency. A single request may have dozens of events, especially during bulk imports or workflows.

async function processHubSpotEvents(events) {
  // Group events by type for batch processing
  const eventsByType = events.reduce((acc, event) => {
    const type = event.subscriptionType;
    if (!acc[type]) acc[type] = [];
    acc[type].push(event);
    return acc;
  }, {});

  // Process each type in parallel
  const processors = Object.entries(eventsByType).map(([type, typeEvents]) => {
    return processBatchByType(type, typeEvents);
  });

  await Promise.all(processors);
}

async function processBatchByType(type, events) {
  // Fetch all records in one batch API call
  const records = await hubspotClient.crm.contacts.batchApi.read({
    inputs: events.map(e => ({ id: e.objectId })),
    properties: ['email', 'firstname', 'lastname']
  });

  for (const record of records.results) {
    await processRecord(type, record);
  }
}

Deduplication

HubSpot may send duplicates. Use idempotent handlers with event IDs:

const processedEvents = new Set(); // Use Redis in production

async function processEventWithDeduplication(event) {
  const eventKey = `${event.eventId}-${event.subscriptionType}`;

  if (processedEvents.has(eventKey)) {
    console.log('Skipping duplicate event:', eventKey);
    return;
  }

  processedEvents.add(eventKey);
  await processEvent(event);
}

Production Best Practices

Respond immediately: HubSpot expects 200 within 5 seconds.

Process asynchronously: Queue for background processing to avoid timeout issues.

Handle partial failures: Log failures and continue batch processing.

Monitor health: Track success rates, processing times, errors. HubSpot disables failing subscriptions.

Implement retries: Retry failed events with exponential backoff.

Simplifying CRM Webhooks with Hook Mesh

Hook Mesh provides managed CRM webhook delivery with automatic signature verification, intelligent retry policies, and event queuing for batched delivery. Let your team focus on business logic instead of infrastructure.

Conclusion

HubSpot webhooks enable real-time CRM workflows. Success requires understanding subscriptions, handling batched events, and verifying signatures.

Start with core event types, implement async processing from day one, and consider managed infrastructure as complexity grows.

See SendGrid email webhooks for delivery tracking or Slack webhooks for team notifications. Browse our Platform Integration Hub.

Related Posts