Integrating Multiple CRMs: A Tale of Salesforce, HubSpot & Dynamics
by Hamzah Ejaz, Software Engineer
At ClientPoint, I architected integrations with Salesforce, HubSpot, and Microsoft Dynamics, automating data synchronization for 2,500+ customer records. Here's how we built a robust, scalable integration layer.
The Challenge
Sales teams were losing hours to manual data entry across multiple systems. We needed to:
- Sync contacts, deals, and activities bidirectionally
- Handle conflicts and data inconsistencies
- Maintain real-time updates
- Support custom fields and complex data structures
- Ensure data integrity across platforms
Architecture Overview
Unified Integration Layer
// Abstract CRM interface
interface CRMProvider {
authenticate(): Promise<void>
syncContacts(contacts: Contact[]): Promise<SyncResult>
webhookHandler(event: WebhookEvent): Promise<void>
mapFields(source: any, target: string): any
}
class SalesforceCRM implements CRMProvider {
// Salesforce-specific implementation
}
class HubSpotCRM implements CRMProvider {
// HubSpot-specific implementation
}
Key Integration Patterns
1. Bidirectional Sync with Conflict Resolution
async function syncRecord(record: Record, source: CRM, target: CRM) {
const lastModified = await getLastModified(record.id)
if (record.updatedAt > lastModified.target) {
// Source is newer - sync to target
await target.update(record)
} else if (record.updatedAt < lastModified.target) {
// Target is newer - sync from target
const targetRecord = await target.get(record.id)
await source.update(targetRecord)
} else {
// Conflict - use business rules
const resolved = await resolveConflict(record, lastModified)
await syncBoth(resolved, source, target)
}
}
2. Webhook Management
app.post('/webhooks/salesforce', async (req, res) => {
const event = req.body
// Validate webhook signature
if (!validateSignature(event, req.headers['x-sf-signature'])) {
return res.status(401).send('Invalid signature')
}
// Queue for processing
await queue.add('crm-sync', {
source: 'salesforce',
event,
timestamp: Date.now()
})
res.status(200).send('Accepted')
})
3. Field Mapping Engine
const fieldMappings = {
salesforce: {
'Email': 'email',
'Phone': 'phone',
'Company__c': 'company',
},
hubspot: {
'email': 'email',
'phone': 'phone',
'company': 'company',
}
}
function mapFields(data: any, sourceCRM: string, targetCRM: string) {
const mapped = {}
const sourceMap = fieldMappings[sourceCRM]
const targetMap = fieldMappings[targetCRM]
for (const [sourceField, commonField] of Object.entries(sourceMap)) {
if (data[sourceField]) {
const targetField = Object.keys(targetMap).find(
key => targetMap[key] === commonField
)
if (targetField) {
mapped[targetField] = data[sourceField]
}
}
}
return mapped
}
Production Learnings
Rate Limiting Strategies
class RateLimitedAPI {
private queue: PQueue
constructor(requestsPerMinute: number) {
this.queue = new PQueue({
interval: 60000,
intervalCap: requestsPerMinute,
})
}
async call(fn: () => Promise<any>) {
return this.queue.add(fn)
}
}
const salesforceAPI = new RateLimitedAPI(100) // 100 req/min
const hubspotAPI = new RateLimitedAPI(150) // 150 req/min
Error Handling & Retries
async function syncWithRetry(operation: () => Promise<any>, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation()
} catch (error) {
if (attempt === maxRetries) throw error
const delay = Math.pow(2, attempt) * 1000 // Exponential backoff
await new Promise(resolve => setTimeout(resolve, delay))
logger.warn(`Retry attempt ${attempt} after error:`, error.message)
}
}
}
Data Validation
const ContactSchema = z.object({
email: z.string().email(),
phone: z.string().optional(),
firstName: z.string().min(1),
lastName: z.string().min(1),
company: z.string().optional(),
})
async function validateAndSync(contact: unknown) {
try {
const validated = ContactSchema.parse(contact)
await syncContact(validated)
} catch (error) {
logger.error('Validation failed:', error)
await notifyAdmin(contact, error)
}
}
Performance Optimizations
Batch Processing
async function batchSync(records: Record[], batchSize = 200) {
const batches = chunk(records, batchSize)
for (const batch of batches) {
await Promise.all(batch.map(record => syncRecord(record)))
await sleep(1000) // Rate limiting
}
}
Caching Layer
const cache = new Redis()
async function getCachedContact(id: string) {
const cached = await cache.get(`contact:${id}`)
if (cached) return JSON.parse(cached)
const contact = await fetchFromCRM(id)
await cache.setex(`contact:${id}`, 3600, JSON.stringify(contact))
return contact
}
Results
- 2,500+ records synchronized automatically
- Zero data loss with comprehensive error handling
- 99.9% sync accuracy with conflict resolution
- Eliminated manual entry saving 10+ hours/week per team
Key Takeaways
- Abstract early: Build a common interface before integrating specific CRMs
- Validate everything: CRMs have different validation rules - normalize data
- Monitor proactively: Set up alerts for sync failures
- Document mappings: Field mappings are business logic - maintain documentation
- Test edge cases: Deleted records, duplicates, and network failures need handling
Enterprise integrations are complex, but with the right patterns, they become manageable and reliable.