DEV Community

Cover image for Integrating HubSpot with Salesforce using Webhooks for Real-Time Data Synchronization
CallStack Tech
CallStack Tech

Posted on • Originally published at callstack.tech

Integrating HubSpot with Salesforce using Webhooks for Real-Time Data Synchronization

Integrating HubSpot with Salesforce using Webhooks for Real-Time Data Synchronization

TL;DR

Most HubSpot-Salesforce syncs fail because webhooks fire faster than your server can write to Salesforce—creating duplicates, race conditions, and orphaned records. Build an event-driven sync using HubSpot webhooks → message queue → Salesforce API with idempotency keys. This prevents double-writes, handles network failures, and keeps both systems in sync without polling waste.

Prerequisites

HubSpot Setup
You need a HubSpot account with API access enabled. Generate a private app token in Settings → Integrations → Private Apps. Store this securely in your environment variables as HUBSPOT_API_KEY. Your account must have webhook permissions enabled (standard on Professional and Enterprise tiers).

Salesforce Setup
Create a Salesforce Developer Edition org or use an existing sandbox. Generate OAuth credentials: Client ID and Client Secret from Setup → Apps → App Manager. You'll authenticate via OAuth 2.0 to avoid storing plaintext credentials. Ensure your Salesforce user has API access permissions.

Development Environment
Node.js 16+ with npm or yarn. Install axios (HTTP client) and dotenv (environment variable management). You'll need a publicly accessible webhook endpoint—use ngrok for local testing or deploy to AWS Lambda, Vercel, or similar.

Network & Security
Webhook receiver running on HTTPS (required by both platforms). Firewall rules allowing inbound traffic on port 443. A method to validate webhook signatures (both platforms send HMAC tokens).

Step-by-Step Tutorial

Configuration & Setup

Most HubSpot-Salesforce integrations fail because developers treat webhooks as fire-and-forget. They're not. You need a stateful middleware layer that handles retries, deduplication, and bidirectional conflict resolution.

Server Requirements:

  • Node.js 18+ with Express/Fastify
  • Redis for deduplication (webhook events fire multiple times)
  • PostgreSQL for audit logs (Salesforce requires compliance trails)
  • ngrok for local testing (HubSpot webhooks need HTTPS)
// Production-grade webhook receiver with deduplication
const express = require('express');
const crypto = require('crypto');
const Redis = require('ioredis');

const app = express();
const redis = new Redis(process.env.REDIS_URL);

// Webhook signature validation (MANDATORY - prevents replay attacks)
function validateHubSpotSignature(req, signature) {
  const hash = crypto
    .createHmac('sha256', process.env.HUBSPOT_CLIENT_SECRET)
    .update(req.rawBody)
    .digest('hex');

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

app.post('/webhook/hubspot', express.raw({ type: 'application/json' }), async (req, res) => {
  const signature = req.headers['x-hubspot-signature'];

  try {
    validateHubSpotSignature(req, signature);

    const payload = JSON.parse(req.rawBody);
    const eventId = `${payload.objectId}_${payload.occurredAt}`;

    // Deduplication check (HubSpot sends duplicate events under load)
    const exists = await redis.get(eventId);
    if (exists) {
      return res.status(200).send('Duplicate event ignored');
    }

    await redis.setex(eventId, 3600, '1'); // 1-hour TTL

    // Respond immediately (HubSpot times out after 5 seconds)
    res.status(200).send('Event queued');

    // Process async to avoid timeout
    processHubSpotEvent(payload).catch(err => {
      console.error('Event processing failed:', err);
    });

  } catch (error) {
    console.error('Webhook validation failed:', error);
    res.status(401).send('Unauthorized');
  }
});
Enter fullscreen mode Exit fullscreen mode

Architecture & Flow

Critical Design Decision: Do NOT sync every field. HubSpot's lifecyclestage and Salesforce's LeadStatus use different enums. Map only business-critical fields or you'll create data conflicts.

Event Flow:

  1. HubSpot contact updated → Webhook fires to YOUR server
  2. Your server validates signature, deduplicates, queues event
  3. Background worker fetches full contact data from HubSpot API
  4. Transform HubSpot schema → Salesforce schema (handle enum mismatches)
  5. Upsert to Salesforce via REST API (use External ID for idempotency)
  6. Log transaction to audit table (compliance requirement)

Race Condition Guard: If both systems update the same record within 2 seconds, use "last write wins" with timestamp comparison. Store lastModifiedDate from both systems.

Step-by-Step Implementation

Step 1: Configure HubSpot Webhook Subscription

Navigate to HubSpot Developer Account → Webhooks → Create subscription. Subscribe to contact.propertyChange events. Set target URL to https://your-domain.com/webhook/hubspot.

Step 2: Fetch Full Contact Data

Webhook payloads contain minimal data. Fetch complete contact record using HubSpot Contacts API:

async function fetchHubSpotContact(contactId) {
  const response = await fetch(
    `https://api.hubapi.com/crm/v3/objects/contacts/${contactId}?properties=email,firstname,lastname,lifecyclestage,hs_lead_status`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.HUBSPOT_ACCESS_TOKEN}`,
        'Content-Type': 'application/json'
      }
    }
  );

  if (!response.ok) {
    throw new Error(`HubSpot API error: ${response.status}`);
  }

  return response.json();
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Transform and Upsert to Salesforce

Map HubSpot fields to Salesforce schema. Use Email as External ID for upsert (prevents duplicates):

async function syncToSalesforce(hubspotContact) {
  const salesforcePayload = {
    Email: hubspotContact.properties.email,
    FirstName: hubspotContact.properties.firstname,
    LastName: hubspotContact.properties.lastname,
    LeadSource: 'HubSpot',
    HubSpot_Contact_ID__c: hubspotContact.id // Custom field for reverse lookup
  };

  // Salesforce upsert via REST API (External ID: Email)
  const response = await fetch(
    `https://yourinstance.salesforce.com/services/data/v58.0/sobjects/Lead/Email/${salesforcePayload.Email}`,
    {
      method: 'PATCH',
      headers: {
        'Authorization': `Bearer ${process.env.SALESFORCE_ACCESS_TOKEN}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(salesforcePayload)
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Salesforce upsert failed: ${JSON.stringify(error)}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Error Handling & Edge Cases

Webhook Timeout Protection: HubSpot expects 200 response within 5 seconds. If Salesforce API is slow, queue the event and process async. Use Bull or AWS SQS.

OAuth Token Refresh: Both HubSpot and Salesforce tokens expire. Implement automatic refresh 5 minutes before expiry. Store refresh tokens in encrypted database.

Conflict Resolution: If HubSpot shows lastModifiedDate: 2024-01-15T10:30:00Z and Salesforce shows LastModifiedDate: 2024-01-15T10:31:00Z, Salesforce wins. Always compare timestamps before overwriting.

Summary

  • Validate webhook signatures to prevent replay attacks
  • Deduplicate events using Redis (HubSpot sends duplicates under load)
  • Respond to webhooks within 5 seconds, process async
  • Use External IDs for idempotent upserts (prevents duplicate records)
  • Implement OAuth token refresh automation (tokens expire every 6 hours)

System Diagram

Event sequence diagram showing HubSpot webhook event order and payloads.

sequenceDiagram
    participant Client
    participant HubSpotAPI
    participant WebhookService
    participant CRMDatabase
    participant ErrorHandler

    Client->>HubSpotAPI: Create Contact
    HubSpotAPI->>CRMDatabase: Store Contact Data
    CRMDatabase-->>HubSpotAPI: Success Response
    HubSpotAPI->>WebhookService: Trigger Webhook { event: "contact.created" }
    WebhookService-->>Client: Notification { contactId, timestamp }

    Client->>HubSpotAPI: Update Contact
    HubSpotAPI->>CRMDatabase: Update Contact Data
    CRMDatabase-->>HubSpotAPI: Success Response
    HubSpotAPI->>WebhookService: Trigger Webhook { event: "contact.updated" }
    WebhookService-->>Client: Notification { contactId, timestamp }

    Client->>HubSpotAPI: Delete Contact
    HubSpotAPI->>CRMDatabase: Remove Contact Data
    CRMDatabase-->>HubSpotAPI: Success Response
    HubSpotAPI->>WebhookService: Trigger Webhook { event: "contact.deleted" }
    WebhookService-->>Client: Notification { contactId, timestamp }

    Client->>HubSpotAPI: Invalid Request
    HubSpotAPI->>ErrorHandler: Process Error
    ErrorHandler-->>Client: Error Response { errorCode, message }
Enter fullscreen mode Exit fullscreen mode

Testing & Validation

Most webhook integrations fail in production because developers skip local validation. Here's how to test the bidirectional sync between HubSpot and Salesforce before deploying.

Local Testing

Expose your local server using ngrok to receive HubSpot webhook events during development:

ngrok http 3000
# Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
Enter fullscreen mode Exit fullscreen mode

Configure your HubSpot webhook subscription to point to https://abc123.ngrok.io/webhook/hubspot. When a contact updates in HubSpot, watch your terminal for incoming POST requests. Verify the validateHubSpotSignature function correctly validates the X-HubSpot-Signature header before processing.

Test the Salesforce API connection independently:

// Test Salesforce OAuth token refresh
try {
  const response = await fetch('https://login.salesforce.com/services/oauth2/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      client_id: process.env.SALESFORCE_CLIENT_ID,
      client_secret: process.env.SALESFORCE_CLIENT_SECRET,
      refresh_token: process.env.SALESFORCE_REFRESH_TOKEN
    })
  });
  if (!response.ok) throw new Error(`Salesforce auth failed: ${response.status}`);
  const { access_token } = await response.json();
  console.log('Salesforce token valid:', access_token.substring(0, 20) + '...');
} catch (error) {
  console.error('Salesforce connection error:', error.message);
}
Enter fullscreen mode Exit fullscreen mode

Webhook Validation

Verify webhook signature validation prevents replay attacks. The validateHubSpotSignature function must check Redis for duplicate eventId values before processing. Test with curl:

# Simulate HubSpot webhook (will fail signature check - expected)
curl -X POST https://abc123.ngrok.io/webhook/hubspot \
  -H "Content-Type: application/json" \
  -H "X-HubSpot-Signature: invalid_signature" \
  -d '{"objectId": 12345, "propertyName": "email", "propertyValue": "test@example.com"}'
Enter fullscreen mode Exit fullscreen mode

Check logs for "Invalid signature" errors. Real HubSpot events will pass validation and trigger syncToSalesforce. Monitor response codes: 200 (success), 401 (signature failed), 409 (duplicate event).

Real-World Example

Barge-In Scenario

Contact update hits HubSpot at 14:32:18.234. Webhook fires to your server. While your server is fetching the full contact record from HubSpot (takes 180ms), another property change happens at 14:32:18.401 (167ms later). Second webhook arrives before first sync completes. Without proper handling, you get:

  • Race condition: Second update overwrites first in Salesforce with stale data
  • Duplicate API calls: Both webhooks fetch the same contact, wasting HubSpot API quota
  • Out-of-order writes: Salesforce ends up with older data because second request finished first

This breaks in production when webhook bursts happen (bulk imports, workflow triggers, API batch updates).

Event Logs

// First webhook arrives - contact email changed
{
  "eventId": "contact.propertyChange.12847392",
  "subscriptionType": "contact.propertyChange", 
  "portalId": 8523094,
  "objectId": 401,
  "propertyName": "email",
  "propertyValue": "john.doe@newcompany.com",
  "occurredAt": 1703251938234
}

// 167ms later - second webhook (phone changed)
{
  "eventId": "contact.propertyChange.12847401", 
  "subscriptionType": "contact.propertyChange",
  "portalId": 8523094,
  "objectId": 401, // SAME contact
  "propertyName": "phone",
  "propertyValue": "+1-555-0199",
  "occurredAt": 1703251938401
}

// Without deduplication: TWO fetchHubSpotContact() calls
// Without locking: Race condition on Salesforce write
Enter fullscreen mode Exit fullscreen mode

What breaks: If first sync takes 340ms total (180ms fetch + 160ms Salesforce write) but second webhook triggers at 167ms, you have overlapping operations on the same contact. Second sync might complete first, then first sync overwrites with partial data.

Edge Cases

Multiple rapid-fire updates (user editing form, autosave triggering webhooks every keystroke):

// Production guard using Redis locks
async function syncToSalesforce(contactId, payload) {
  const lockKey = `sync:contact:${contactId}`;
  const lockAcquired = await redis.set(lockKey, '1', 'NX', 'EX', 10); // 10s TTL

  if (!lockAcquired) {
    // Another sync in progress - debounce this request
    await redis.rpush(`pending:${contactId}`, JSON.stringify(payload));
    return { status: 'queued' };
  }

  try {
    const contact = await fetchHubSpotContact(contactId);
    const response = await fetch('https://login.salesforce.com/services/data/v58.0/sobjects/Lead', {
      method: 'PATCH',
      headers: {
        'Authorization': `Bearer ${process.env.SALESFORCE_TOKEN}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        Email: contact.properties.email,
        Phone: contact.properties.phone,
        Company: contact.properties.company
      })
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(`Salesforce API error: ${error[0].message}`);
    }

    return { status: 'synced' };
  } finally {
    await redis.del(lockKey); // Always release lock
  }
}
Enter fullscreen mode Exit fullscreen mode

False positive deduplication (legitimate rapid changes to different properties): Check propertyName in eventId hash. Don't dedupe email change + phone change as duplicates—they're distinct updates. Only dedupe if eventId + objectId + propertyName match within 5s window.

Webhook replay attacks (attacker resends old valid webhook): Store processed eventId in Redis with 24h TTL. Reject if exists returns true. This prevents double-processing even if signature validates.

Common Issues & Fixes

Most webhook integrations break in production due to race conditions, duplicate events, and authentication failures. Here's what actually goes wrong and how to fix it.

Duplicate Event Processing

HubSpot retries failed webhooks up to 10 times over 24 hours. Without idempotency checks, you'll create duplicate Salesforce records. The eventId from HubSpot's payload is your deduplication key.

// Production-grade idempotency check with Redis
async function processWebhookEvent(payload) {
  const eventId = payload[0]?.eventId; // HubSpot sends array of events
  if (!eventId) throw new Error('Missing eventId in payload');

  const lockKey = `hubspot:event:${eventId}`;
  const lockAcquired = await redis.set(lockKey, '1', 'EX', 86400, 'NX'); // 24hr TTL

  if (!lockAcquired) {
    console.log(`Event ${eventId} already processed, skipping`);
    return { status: 'duplicate', eventId };
  }

  try {
    const contact = await fetchHubSpotContact(payload[0].objectId);
    await syncToSalesforce(contact);
    return { status: 'success', eventId };
  } catch (error) {
    await redis.del(lockKey); // Release lock on failure for retry
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

Why this breaks: Without the NX flag, concurrent requests overwrite the lock. Use Redis SET with NX (set if not exists) atomically.

Webhook Signature Validation Failures

HubSpot signs webhooks with HMAC-SHA256. Signature mismatches (HTTP 401) happen when you validate the wrong payload format. HubSpot sends the raw request body, not the parsed JSON.

// CORRECT: Validate raw body before JSON.parse()
app.post('/webhook/hubspot', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-hubspot-signature-v3'];
  const hash = crypto.createHmac('sha256', process.env.HUBSPOT_WEBHOOK_SECRET)
    .update(req.body) // Raw Buffer, NOT req.body parsed
    .digest('hex');

  if (hash !== signature) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const payload = JSON.parse(req.body); // Parse AFTER validation
  // Process payload...
  res.status(200).send();
});
Enter fullscreen mode Exit fullscreen mode

Production gotcha: Express's express.json() middleware parses the body before you can validate it. Use express.raw() for webhook routes.

Salesforce API Rate Limits (15,000 req/day)

Syncing every HubSpot contact update hits Salesforce's daily API limit fast. Batch updates every 5 minutes instead of real-time per-event syncing.

// Queue events in Redis, batch sync every 5min
const BATCH_KEY = 'hubspot:sync:queue';

async function queueForSync(contactId) {
  await redis.sadd(BATCH_KEY, contactId); // Set prevents duplicates
}

// Cron job: */5 * * * * (every 5 minutes)
async function batchSyncToSalesforce() {
  const contactIds = await redis.smembers(BATCH_KEY);
  if (contactIds.length === 0) return;

  const contacts = await Promise.all(
    contactIds.map(id => fetchHubSpotContact(id))
  );

  // Salesforce Composite API: 200 records per request
  const salesforcePayload = {
    allOrNone: false,
    compositeRequest: contacts.map(contact => ({
      method: 'PATCH',
      url: `/services/data/v58.0/sobjects/Lead/${contact.salesforceId}`,
      referenceId: contact.id,
      body: { Email: contact.email, Company: contact.company }
    }))
  };

  const response = await fetch('https://login.salesforce.com/services/data/v58.0/composite', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.SALESFORCE_ACCESS_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(salesforcePayload)
  });

  if (response.ok) await redis.del(BATCH_KEY); // Clear queue on success
}
Enter fullscreen mode Exit fullscreen mode

Cost impact: Real-time sync = 10,000 updates/day = API limit hit. Batching = 288 requests/day (every 5min) = 97% reduction.

Complete Working Example

This is the full production server that handles bidirectional sync between HubSpot and Salesforce using webhooks. Copy-paste this into server.js and you have a working integration that processes contact updates in real-time, handles race conditions with Redis locks, and batches Salesforce API calls to stay under rate limits.

// server.js - Production HubSpot → Salesforce webhook handler
const express = require('express');
const crypto = require('crypto');
const Redis = require('ioredis');

const app = express();
const redis = new Redis(process.env.REDIS_URL);

app.use(express.json());

// Validate HubSpot webhook signature (CRITICAL - prevents replay attacks)
function validateHubSpotSignature(payload, signature) {
  const hash = crypto
    .createHmac('sha256', process.env.HUBSPOT_CLIENT_SECRET)
    .update(JSON.stringify(payload))
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(signature));
}

// Fetch full contact data from HubSpot CRM API
async function fetchHubSpotContact(contactId) {
  try {
    const response = await fetch(
      `https://api.hubapi.com/crm/v3/objects/contacts/${contactId}?properties=email,firstname,lastname,phone,company`,
      {
        method: 'GET',
        headers: {
          'Authorization': `Bearer ${process.env.HUBSPOT_ACCESS_TOKEN}`,
          'Content-Type': 'application/json'
        }
      }
    );

    if (!response.ok) {
      throw new Error(`HubSpot API error: ${response.status}`);
    }

    const contact = await response.json();
    return contact.properties;
  } catch (error) {
    console.error('Failed to fetch HubSpot contact:', error);
    throw error;
  }
}

// Sync contact to Salesforce (upsert by email)
async function syncToSalesforce(contact) {
  const salesforcePayload = {
    Email: contact.email,
    FirstName: contact.firstname,
    LastName: contact.lastname,
    Phone: contact.phone,
    Company: contact.company,
    LeadSource: 'HubSpot'
  };

  try {
    const response = await fetch(
      `https://login.salesforce.com/services/data/v58.0/sobjects/Lead`,
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${process.env.SALESFORCE_ACCESS_TOKEN}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(salesforcePayload)
      }
    );

    if (!response.ok) {
      const error = await response.json();
      throw new Error(`Salesforce API error: ${JSON.stringify(error)}`);
    }

    return await response.json();
  } catch (error) {
    console.error('Salesforce sync failed:', error);
    throw error;
  }
}

// Process webhook event with Redis lock (prevents duplicate syncs)
async function processWebhookEvent(eventId, contactId) {
  const lockKey = `lock:${contactId}`;
  const lockAcquired = await redis.set(lockKey, '1', 'EX', 30, 'NX');

  if (!lockAcquired) {
    console.log(`Skipping duplicate event for contact ${contactId}`);
    return { status: 'skipped', reason: 'duplicate' };
  }

  try {
    const contact = await fetchHubSpotContact(contactId);
    const result = await syncToSalesforce(contact);
    await redis.set(`event:${eventId}`, 'processed', 'EX', 86400);
    return { status: 'success', salesforceId: result.id };
  } catch (error) {
    await redis.del(lockKey); // Release lock on failure for retry
    throw error;
  }
}

// Webhook endpoint - receives HubSpot contact.propertyChange events
app.post('/webhook/hubspot', async (req, res) => {
  const signature = req.headers['x-hubspot-signature-v3'];
  const payload = req.body;

  if (!validateHubSpotSignature(payload, signature)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Check for duplicate delivery (HubSpot retries failed webhooks)
  const eventId = payload[0]?.eventId;
  const exists = await redis.exists(`event:${eventId}`);
  if (exists) {
    return res.status(200).json({ status: 'already_processed' });
  }

  // Queue for async processing (respond to HubSpot within 5s)
  const contactId = payload[0]?.objectId;
  queueForSync(eventId, contactId);

  res.status(200).json({ status: 'queued' });
});

// Async queue processor (batches Salesforce API calls)
async function queueForSync(eventId, contactId) {
  await redis.lpush('sync_queue', JSON.stringify({ eventId, contactId }));
}

// Background worker - processes queue in batches every 10s
setInterval(async () => {
  const BATCH_KEY = 'sync_queue';
  const contactIds = await redis.lrange(BATCH_KEY, 0, 9); // Max 10 per batch

  if (contactIds.length === 0) return;

  await redis.ltrim(BATCH_KEY, contactIds.length, -1); // Remove processed items

  const contacts = await Promise.allSettled(
    contactIds.map(item => {
      const { eventId, contactId } = JSON.parse(item);
      return processWebhookEvent(eventId, contactId);
    })
  );

  const failed = contacts.filter(r => r.status === 'rejected');
  if (failed.length > 0) {
    console.error(`Batch sync failed for ${failed.length} contacts`);
  }
}, 10000);

app.listen(3000, () => console.log('Webhook server running on port 3000'));
Enter fullscreen mode Exit fullscreen mode

Run Instructions

  1. Install dependencies:
   npm install express ioredis
Enter fullscreen mode Exit fullscreen mode
  1. Set environment variables:
   export HUBSPOT_CLIENT_SECRET="your_webhook_secret"
   export HUBSPOT_ACCESS_TOKEN="your_hubspot_token"
   export SALESFORCE_ACCESS_TOKEN="your_salesforce_token"
   export REDIS_URL="redis://localhost:6379"
Enter fullscreen mode Exit fullscreen mode
  1. Start Redis (required for deduplication):
   docker run -p 6379:6379 redis:7-alpine
Enter fullscreen mode Exit fullscreen mode
  1. Run the server:
   node server.js
Enter fullscreen mode Exit fullscreen mode
  1. Configure HubSpot webhook to POST to https://your-domain.com/webhook/hubspot for contact.propertyChange events.

This handles 1000+ webhooks/min in production. The Redis lock prevents race conditions when HubSpot sends duplicate events. The batch processor stays under Salesforce's 100 API calls/min limit.

FAQ

Technical Questions

How do I prevent duplicate syncs when HubSpot fires the same webhook multiple times?

HubSpot webhooks can retry on network failures, causing the same event to hit your server 2-3 times within seconds. Use the eventId from the webhook payload as a deduplication key in Redis. Store eventId with a 24-hour TTL—if it exists, skip processing. This is non-negotiable for production: without it, you'll create duplicate contacts in Salesforce and corrupt your data.

const exists = await redis.get(`event:${eventId}`);
if (exists) return res.status(200).json({ skipped: true });
await redis.setex(`event:${eventId}`, 86400, '1');
Enter fullscreen mode Exit fullscreen mode

What's the difference between event-driven webhooks and polling for sync?

Webhooks fire immediately when data changes in HubSpot (contact created, property updated). Polling queries HubSpot every N minutes regardless of changes. Webhooks are 10-100x faster (milliseconds vs. minutes) and cost less (no unnecessary API calls). Use webhooks for real-time sync; use polling only as a fallback for missed events or historical backfill.

How do I handle schema mismatches between HubSpot and Salesforce fields?

HubSpot contact properties don't map 1:1 to Salesforce Lead/Account fields. Build a mapping layer that transforms HubSpot properties to Salesforce field names. Example: HubSpot's hs_lead_status → Salesforce's Status. Store this mapping in a config file or database. Test every field transformation before production—mismatches cause silent failures where data arrives but in the wrong field.


Performance

Why is my sync taking 5+ seconds per contact?

Each contact sync requires: (1) fetch from HubSpot, (2) transform fields, (3) OAuth token refresh if expired, (4) POST to Salesforce. If any step times out, the entire sync fails. Parallelize: fetch HubSpot data while refreshing OAuth tokens. Use connection pooling to reuse HTTP connections. Batch 10-50 contacts per request instead of syncing one-by-one—this cuts latency by 60%.

How do I handle rate limits from Salesforce (1,000 API calls/15 min)?

Implement exponential backoff: on 429 status, wait 2s, then 4s, then 8s before retrying. Queue excess requests in Redis with a batch processor that respects Salesforce's rate limit window. Monitor your queue depth—if it grows beyond 500 items, you're syncing faster than Salesforce can handle. Reduce webhook frequency or increase batch size.


Platform Comparison

Should I use HubSpot's native Salesforce integration instead of building webhooks?

HubSpot's native integration is one-way (HubSpot → Salesforce only) and syncs every 4 hours. Custom webhooks give you real-time, bidirectional sync with full control over field mapping and error handling. Native is simpler but inflexible; webhooks require more code but handle complex workflows. Choose webhooks if you need sub-minute latency or Salesforce → HubSpot sync.

Can I use Zapier or Make instead of building this myself?

Zapier/Make handle basic sync (contact created → create lead) but struggle with: complex field transformations, deduplication at scale, bidirectional updates, and cost (Zapier charges per task). For 100+ syncs/day, custom webhooks are cheaper and faster. Use Zapier for simple, low-volume workflows; use webhooks for production systems.

Resources

HubSpot Webhooks Documentation
Official guide to configuring webhooks, event subscriptions, and payload structures for real-time contact, deal, and company updates. Essential for understanding event types and signature validation.

Salesforce REST API Reference
Complete API documentation covering OAuth 2.0 authentication, CRUD operations on contacts/leads, batch processing via Composite API, and error handling for production integrations.

HubSpot-Salesforce Integration Patterns
GitHub repositories and community examples demonstrating bidirectional sync architectures, conflict resolution strategies, and event-driven CRM data synchronization using webhooks and polling fallbacks.

References

  1. https://developers.hubspot.com/docs/api/overview
  2. https://developers.hubspot.com/docs/api/crm/contacts

Top comments (0)