CRM Integration Guide

Seamlessly integrate LinkedIn intelligence into your CRM for automated data enrichment and activity tracking.

Why Integrate with Your CRM?

  • β€’ Single source of truth - All customer data in one place
  • β€’ Automated workflows - Trigger actions based on LinkedIn changes
  • β€’ Better insights - Combine LinkedIn data with sales activities
  • β€’ Team alignment - Everyone sees the same intelligence

Integration Architecture

A typical CRM integration involves these components:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Sales Webhooks │────▢│ Your Middleware │────▢│    Your CRM     β”‚
β”‚      API        β”‚     β”‚   Application   β”‚     β”‚   (Salesforce,  β”‚
β”‚                 β”‚     β”‚                 β”‚     β”‚    HubSpot,     β”‚
β”‚   Webhooks ────────┐  β”‚  - Validation   β”‚     β”‚    Pipedrive)   β”‚
β”‚                 β”‚  β”‚  β”‚  - Mapping      β”‚     β”‚                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚  - Enrichment   β”‚     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚  β”‚  - Queueing     β”‚
                     └─▢│                 β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            

Popular CRM Integrations

Salesforce Integration

Complete integration example for Salesforce:

// 1. Install dependencies
// npm install jsforce @saleswebhooks/node

import jsforce from 'jsforce';
import { SalesWebhooks } from '@saleswebhooks/node';

// Initialize clients
const sf = new jsforce.Connection({
  loginUrl: process.env.SF_LOGIN_URL,
  version: '57.0'
});

await sf.login(
  process.env.SF_USERNAME,
  process.env.SF_PASSWORD + process.env.SF_TOKEN
);

const swClient = new SalesWebhooks({
  apiKey: process.env.SALESWEBHOOKS_API_KEY
});

// 2. Sync Salesforce contacts to Sales Webhooks
async function syncContactsToSalesWebhooks() {
  // Query contacts with LinkedIn URLs
  const contacts = await sf.query(`
    SELECT Id, Name, LinkedIn_URL__c, AccountId, Title, Email
    FROM Contact
    WHERE LinkedIn_URL__c != null
    AND Monitor_LinkedIn__c = true
  `);
  
  // Create subscriptions
  for (const contact of contacts.records) {
    try {
      const subscription = await swClient.subscriptions.create({
        entity_type: 'contact',
        linkedin_url: contact.LinkedIn_URL__c
      });
      
      // Store subscription ID in Salesforce
      await sf.sobject('Contact').update({
        Id: contact.Id,
        LinkedIn_Subscription_ID__c: subscription.id,
        LinkedIn_Monitor_Status__c: 'Active'
      });
    } catch (error) {
      console.error(`Failed to subscribe ${contact.Name}:`, error);
    }
  }
}

// 3. Handle webhooks and update Salesforce
app.post('/salesforce-webhook', async (req, res) => {
  const event = req.body;
  
  try {
    switch (event.type) {
      case 'contact.position_changed':
        await handleJobChange(event.data);
        break;
      case 'contact.company_changed':
        await handleCompanyChange(event.data);
        break;
      case 'contact.post_created':
        await handleNewPost(event.data);
        break;
    }
    
    res.status(200).send('OK');
  } catch (error) {
    console.error('Webhook processing error:', error);
    res.status(500).send('Error');
  }
});

async function handleJobChange(data) {
  // Find contact by LinkedIn URL
  const contacts = await sf.query(`
    SELECT Id, AccountId, OwnerId
    FROM Contact
    WHERE LinkedIn_URL__c = '${data.entity.linkedin_url}'
    LIMIT 1
  `);
  
  if (contacts.records.length === 0) return;
  
  const contact = contacts.records[0];
  const changes = data.changes;
  
  // Update contact record
  await sf.sobject('Contact').update({
    Id: contact.Id,
    Title: changes.current_title,
    Previous_Title__c: changes.previous_title,
    LinkedIn_Last_Updated__c: new Date()
  });
  
  // Create activity record
  await sf.sobject('Task').create({
    WhoId: contact.Id,
    WhatId: contact.AccountId,
    Subject: `Job Change: ${changes.previous_title} β†’ ${changes.current_title}`,
    Description: `${data.entity.name} changed roles from ${changes.previous_title} at ${changes.previous_company} to ${changes.current_title} at ${changes.current_company}`,
    Status: 'Open',
    Priority: changes.is_promotion ? 'High' : 'Normal',
    ActivityDate: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), // 3 days
    OwnerId: contact.OwnerId,
    Type: 'LinkedIn Intelligence'
  });
  
  // Update opportunity if contact is key stakeholder
  if (changes.current_company !== changes.previous_company) {
    await handleContactLeftCompany(contact, changes);
  }
}

HubSpot Integration

Integration example for HubSpot CRM:

// HubSpot integration using their API v3
import { Client } from '@hubspot/api-client';

const hubspot = new Client({ 
  accessToken: process.env.HUBSPOT_ACCESS_TOKEN 
});

// 1. Create custom properties in HubSpot
async function setupHubSpotProperties() {
  // Contact properties
  await hubspot.crm.properties.coreApi.create('contacts', {
    name: 'linkedin_url',
    label: 'LinkedIn URL',
    type: 'string',
    fieldType: 'text',
    groupName: 'linkedin_intelligence'
  });
  
  await hubspot.crm.properties.coreApi.create('contacts', {
    name: 'linkedin_monitor_status',
    label: 'LinkedIn Monitor Status',
    type: 'enumeration',
    fieldType: 'select',
    options: [
      { label: 'Active', value: 'active' },
      { label: 'Inactive', value: 'inactive' },
      { label: 'Error', value: 'error' }
    ],
    groupName: 'linkedin_intelligence'
  });
  
  // Company properties
  await hubspot.crm.properties.coreApi.create('companies', {
    name: 'linkedin_employee_count',
    label: 'LinkedIn Employee Count',
    type: 'number',
    fieldType: 'number',
    groupName: 'linkedin_intelligence'
  });
}

// 2. Sync contacts from HubSpot
async function syncHubSpotContacts() {
  const contacts = await hubspot.crm.contacts.searchApi.doSearch({
    filterGroups: [{
      filters: [{
        propertyName: 'linkedin_url',
        operator: 'HAS_PROPERTY'
      }]
    }],
    properties: ['firstname', 'lastname', 'linkedin_url', 'email'],
    limit: 100
  });
  
  for (const contact of contacts.results) {
    if (contact.properties.linkedin_url) {
      const subscription = await swClient.subscriptions.create({
        entity_type: 'contact',
        linkedin_url: contact.properties.linkedin_url
      });
      
      // Update HubSpot contact
      await hubspot.crm.contacts.basicApi.update(contact.id, {
        properties: {
          linkedin_subscription_id: subscription.id,
          linkedin_monitor_status: 'active'
        }
      });
    }
  }
}

// 3. Process webhooks for HubSpot
async function processHubSpotWebhook(event) {
  // Find contact
  const searchResult = await hubspot.crm.contacts.searchApi.doSearch({
    filterGroups: [{
      filters: [{
        propertyName: 'linkedin_url',
        operator: 'EQ',
        value: event.data.entity.linkedin_url
      }]
    }],
    limit: 1
  });
  
  if (searchResult.results.length === 0) return;
  
  const contactId = searchResult.results[0].id;
  
  // Create timeline event
  await createTimelineEvent(contactId, event);
  
  // Update properties based on event type
  if (event.type === 'contact.position_changed') {
    await hubspot.crm.contacts.basicApi.update(contactId, {
      properties: {
        jobtitle: event.data.changes.current_title,
        company: event.data.changes.current_company,
        linkedin_last_job_change: new Date().toISOString()
      }
    });
    
    // Create task for sales rep
    await createHubSpotTask(contactId, {
      subject: `Follow up: ${event.data.entity.name} changed jobs`,
      notes: `New role: ${event.data.changes.current_title} at ${event.data.changes.current_company}`,
      priority: 'HIGH'
    });
  }
}

// 4. HubSpot workflow automation
async function createTimelineEvent(contactId, event) {
  const eventTemplate = {
    eventTemplateId: '1234567', // Your custom timeline event template
    objectId: contactId,
    tokens: {
      event_type: event.type,
      description: generateEventDescription(event),
      linkedin_url: event.data.entity.linkedin_url
    }
  };
  
  await hubspot.crm.timeline.eventsApi.create(eventTemplate);
}

Pipedrive Integration

Integration for Pipedrive CRM:

// Pipedrive integration
import Pipedrive from 'pipedrive';

const pd = new Pipedrive.Client(process.env.PIPEDRIVE_API_TOKEN);

// 1. Set up custom fields
async function setupPipedriveFields() {
  // Create person fields
  await pd.personFields.add({
    name: 'LinkedIn URL',
    field_type: 'text'
  });
  
  await pd.personFields.add({
    name: 'LinkedIn Status',
    field_type: 'enum',
    options: ['Active', 'Inactive', 'Changed']
  });
  
  // Create organization fields
  await pd.organizationFields.add({
    name: 'LinkedIn Company URL',
    field_type: 'text'
  });
}

// 2. Webhook handler for Pipedrive
async function handlePipedriveWebhook(event) {
  // Find person by LinkedIn URL
  const persons = await pd.persons.search({
    term: event.data.entity.linkedin_url,
    search_by_email: 0,
    exact_match: 1
  });
  
  if (!persons.data || persons.data.length === 0) return;
  
  const person = persons.data[0];
  
  if (event.type === 'contact.position_changed') {
    // Update person
    await pd.persons.update(person.id, {
      job_title: event.data.changes.current_title,
      'custom_field_key': event.data.changes.current_company
    });
    
    // Create activity
    await pd.activities.add({
      person_id: person.id,
      subject: 'LinkedIn Job Change Alert',
      type: 'linkedin_update',
      note: `${person.name} is now ${event.data.changes.current_title} at ${event.data.changes.current_company}`,
      due_date: new Date().toISOString().split('T')[0],
      done: 0
    });
    
    // Update deal if exists
    if (person.open_deals_count > 0) {
      await updateDealRisk(person, event.data.changes);
    }
  }
}

Generic CRM Integration Pattern

Build a flexible integration that works with any CRM:

// Generic CRM integration middleware
class CRMIntegration {
  constructor(crmAdapter, salesWebhooksClient) {
    this.crm = crmAdapter;
    this.sw = salesWebhooksClient;
  }
  
  async syncContacts() {
    // Get contacts from CRM
    const contacts = await this.crm.getContactsWithLinkedIn();
    
    // Create subscriptions
    const results = {
      success: 0,
      failed: 0,
      errors: []
    };
    
    for (const contact of contacts) {
      try {
        // Check if already subscribed
        const existing = await this.checkExistingSubscription(contact);
        if (existing) continue;
        
        // Create subscription
        const subscription = await this.sw.subscriptions.create({
          entity_type: 'contact',
          linkedin_url: contact.linkedinUrl
        });
        
        // Update CRM
        await this.crm.updateContact(contact.id, {
          linkedin_subscription_id: subscription.id,
          linkedin_status: 'active'
        });
        
        results.success++;
      } catch (error) {
        results.failed++;
        results.errors.push({
          contact: contact.id,
          error: error.message
        });
      }
    }
    
    return results;
  }
  
  async processWebhook(event) {
    // Map Sales Webhooks event to CRM format
    const crmData = this.mapEventToCRM(event);
    
    // Find related record
    const record = await this.crm.findByLinkedIn(
      event.data.entity.linkedin_url
    );
    
    if (!record) {
      console.log('No CRM record found for:', event.data.entity.linkedin_url);
      return;
    }
    
    // Update record
    await this.crm.updateRecord(record.type, record.id, crmData.updates);
    
    // Create activity
    if (crmData.activity) {
      await this.crm.createActivity(record, crmData.activity);
    }
    
    // Trigger workflows
    if (crmData.triggers) {
      for (const trigger of crmData.triggers) {
        await this.crm.triggerWorkflow(trigger.name, {
          recordId: record.id,
          data: trigger.data
        });
      }
    }
  }
  
  mapEventToCRM(event) {
    const mapping = {
      'contact.position_changed': {
        updates: {
          title: event.data.changes.current_title,
          company: event.data.changes.current_company,
          last_linkedin_update: new Date()
        },
        activity: {
          type: 'linkedin_change',
          subject: 'Job Change Detected',
          description: this.formatJobChange(event.data)
        },
        triggers: [{
          name: 'job_change_workflow',
          data: event.data.changes
        }]
      },
      'contact.post_created': {
        updates: {
          last_linkedin_activity: new Date(),
          linkedin_engagement_score: '+1'
        },
        activity: {
          type: 'linkedin_post',
          subject: 'LinkedIn Post',
          description: event.data.post.content,
          link: event.data.post.url
        }
      }
    };
    
    return mapping[event.type] || {};
  }
}

Best Practices for CRM Integration

⚑ Integration Best Practices

  • β€’ Use queues - Process webhooks asynchronously to avoid timeouts
  • β€’ Handle duplicates - Check for existing records before creating
  • β€’ Map fields carefully - Maintain data quality and consistency
  • β€’ Log everything - Track sync status and errors for debugging
  • β€’ Respect rate limits - Both for Sales Webhooks and your CRM

Field Mapping Reference

Sales Webhooks Field Salesforce HubSpot Pipedrive
entity.linkedin_url LinkedIn_URL__c linkedin_url custom_field
changes.current_title Title jobtitle job_title
changes.current_company Account (lookup) company org_id
post.content Task.Description note activity.note

Error Handling

// Robust error handling for CRM sync
async function syncWithRetry(contact, maxAttempts = 3) {
  let lastError;
  
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      // Attempt sync
      const result = await performSync(contact);
      
      // Log success
      await logSyncResult({
        contact_id: contact.id,
        status: 'success',
        attempt: attempt,
        subscription_id: result.subscription_id
      });
      
      return result;
    } catch (error) {
      lastError = error;
      
      // Log failure
      await logSyncResult({
        contact_id: contact.id,
        status: 'failed',
        attempt: attempt,
        error: error.message
      });
      
      // Determine if retry is appropriate
      if (!isRetryableError(error)) {
        throw error;
      }
      
      // Wait before retry
      if (attempt < maxAttempts) {
        await sleep(Math.pow(2, attempt) * 1000);
      }
    }
  }
  
  // Max attempts reached
  throw new Error(`Sync failed after ${maxAttempts} attempts: ${lastError.message}`);
}

function isRetryableError(error) {
  // Network errors
  if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT') {
    return true;
  }
  
  // Rate limits
  if (error.status === 429) {
    return true;
  }
  
  // Temporary CRM errors
  if (error.message.includes('temporarily unavailable')) {
    return true;
  }
  
  return false;
}

Next Steps

With your CRM integration complete:

Need help with your CRM?

Our team can help you build a custom integration for your specific CRM and use case.

Contact Integration Team β†’