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:
- Set up webhook security for production
- Review best practices for scaling
- Explore troubleshooting guide for common issues
Need help with your CRM?
Our team can help you build a custom integration for your specific CRM and use case.
Contact Integration Team β