Skip to main content

Contact Sync Patterns

Overview

Contact sync keeps Genesys and your CRM (Salesforce, Dynamics, etc.) in sync. This guide covers two approaches:

  1. One-way sync (CRM → Genesys, simpler, recommended first)
  2. Bi-directional sync (both ways, complex, conflict resolution needed)

Strategy 1: One-Way Sync (CRM → Genesys)

Concept

Salesforce is the master. Genesys is a read-only mirror.

Salesforce (Master)
    ↓ (one direction)
Genesys (Mirror)

If contact changes in SF → update Genesys
If contact changes in Genesys → ignore (no write-back)

Pros

  • ✅ Simpler (no conflict resolution)
  • ✅ Faster (no locking)
  • ✅ No race conditions
  • ✅ Genesys data always matches SF

Cons

  • ❌ Any changes in Genesys are lost on next sync
  • ❌ Agents can't update CRM from Genesys

Sync Flow

Every 30 minutes:
  ↓
1. Query SF for changes:
   "SELECT * FROM Contact WHERE LastModifiedDate >= 30 min ago"
  ↓
2. For each contact:
   a. Check if exists in Genesys (by email, phone, external ID)
   b. If exists → PATCH (update only changed fields)
   c. If not exists → POST (create new)
  ↓
3. Log results (created, updated, skipped, errors)
  ↓
4. Alert if failures

Implementation

// sync-one-way.js

async function syncSalesforceToGenesys() {
  // 1. Fetch modified contacts from Salesforce
  const sfContacts = await querySalesforce(`
    SELECT Id, FirstName, LastName, Email, Phone
    FROM Contact
    WHERE LastModifiedDate >= LAST_N_MINUTES:30
  `);

  // 2. Get existing Genesys contacts
  const genesysContacts = await fetchGenesysContacts();
  const emailIndex = indexBy(genesysContacts, 'email');

  // 3. Process each SF contact
  let created = 0, updated = 0;
  
  for (const sfContact of sfContacts) {
    const genesysContact = emailIndex.get(sfContact.Email);

    if (genesysContact) {
      // Update existing
      await patchContact(genesysContact.id, {
        firstName: sfContact.FirstName,
        lastName: sfContact.LastName,
        phoneNumbers: [{ number: sfContact.Phone, type: 'WORK' }]
      });
      updated++;
    } else {
      // Create new
      await postContact({
        firstName: sfContact.FirstName,
        lastName: sfContact.LastName,
        email: sfContact.Email,
        phoneNumbers: [{ number: sfContact.Phone, type: 'WORK' }],
        externalId: sfContact.Id  // Link back
      });
      created++;
    }
  }

  console.log(`Created: ${created}, Updated: ${updated}`);
}

When to Use

  • ✅ Initial implementation
  • ✅ Salesforce is single source of truth
  • ✅ Genesys only used for lookups/popups
  • ✅ No need to update contacts from Genesys

Strategy 2: Bi-Directional Sync (Both Ways)

Concept

Both Salesforce AND Genesys can be updated. Sync runs both directions:

Salesforce ←→ Genesys

If SF updated → update Genesys
If Genesys updated → update Salesforce
If both updated at same time → conflict!

Pros

  • ✅ Can update from both sides
  • ✅ True two-way synchronization

Cons

  • ❌ Much more complex
  • ❌ Need conflict resolution
  • ❌ Race conditions possible
  • ❌ Slower (locking needed)
  • ❌ Harder to debug

Conflict Resolution Strategies

Strategy A: Last-Write-Wins

Simple but risky:

Contact updated in both systems at same time:
  SF:     phone changed to 555-1111
  Genesys: phone changed to 555-2222

Winner: Whoever was modified LAST
Loser: Other change is overwritten

Problem: Unpredictable. User's change might be lost.

Strategy B: Field-Level Priority

Different fields have different sources:

contact_name:   SF always wins (SF is master for names)
phone_number:   Last-write-wins (allow edits in both)
tags:           Genesys always wins (agent tags)

Strategy C: Timestamp-Based with Locks

More robust:

1. Read contact with timestamp from both systems
2. Check: Who modified last?
   SF time > Genesys time → SF wins
   Genesys time > SF time → Genesys wins
3. Write winner's data to loser's system
4. Log conflict for human review

Implementation

// sync-bidirectional.js

async function syncBidirectional() {
  // 1. Fetch from both systems
  const sfContacts = await querySalesforce(`
    SELECT Id, FirstName, LastName, Email, LastModifiedDate
    FROM Contact
  `);

  const genesysContacts = await fetchGenesysContacts();

  // 2. Index for matching
  const emailIndex = new Map();
  for (const contact of [...sfContacts, ...genesysContacts]) {
    if (!emailIndex.has(contact.email)) {
      emailIndex.set(contact.email, []);
    }
    emailIndex.get(contact.email).push(contact);
  }

  // 3. Detect conflicts and sync
  let conflicts = [];

  for (const [email, records] of emailIndex) {
    if (records.length === 1) {
      // Only in one system, create in other
      await syncOneWay(records[0]);
    } else if (records.length === 2) {
      // In both systems, detect conflict
      const sf = records.find(r => r.system === 'salesforce');
      const gz = records.find(r => r.system === 'genesys');

      const winner = resolveConflict(sf, gz);

      if (winner === sf) {
        // SF wins, update Genesys
        await updateGenesys(gz.id, sf.data);
      } else {
        // Genesys wins, update SF
        await updateSalesforce(sf.id, gz.data);
      }

      // Log for audit
      if (sf.lastModifiedDate !== gz.dateModified) {
        conflicts.push({ email, sf, gz, winner });
      }
    }
  }

  // 4. Alert on conflicts
  if (conflicts.length > 0) {
    await alertConflicts(conflicts);
  }
}

function resolveConflict(sfContact, gzContact) {
  // Last-write-wins
  const sfTime = new Date(sfContact.LastModifiedDate);
  const gzTime = new Date(gzContact.dateModified);

  if (sfTime > gzTime) {
    console.log(`Conflict: SF wins for ${sfContact.Email}`);
    return sfContact;
  } else {
    console.log(`Conflict: Genesys wins for ${sfContact.Email}`);
    return gzContact;
  }
}

Conflict Resolution Workflow

Conflict Detected
    ↓
Log both versions (SF and Genesys)
    ↓
Apply resolution strategy
    ├─ Last-write-wins?
    ├─ Field-level priority?
    └─ Manual approval?
    ↓
Update loser system
    ↓
Send alert to admin
    ↓
Update audit log

Data Deduplication

Problem: Same person, multiple records

Salesforce:
  Record 1: [email protected]
  Record 2: [email protected] (same person)

Genesys would create 2 records!

Solution: Match on Multiple Fields

function findMatchingContact(sfContact, genesysContacts) {
  // Try email (best match)
  let match = genesysContacts.find(
    gc => gc.email?.toLowerCase() === sfContact.Email.toLowerCase()
  );
  if (match) return match;

  // Try phone (second best)
  match = genesysContacts.find(
    gc => gc.phoneNumbers?.[0]?.number === sfContact.Phone
  );
  if (match) return match;

  // Try first + last name (risky, could be duplicate)
  match = genesysContacts.find(
    gc => gc.firstName === sfContact.FirstName &&
          gc.lastName === sfContact.LastName &&
          gc.externalOrganization?.id === sfContact.AccountId
  );
  if (match) return match;

  // Not found
  return null;
}

Best Practice: External IDs

Salesforce Contact:
  ID: 003xx000003SG
  Email: [email protected]

Genesys Contact:
  ID: contact-123
  externalId: "003xx000003SG"  ← Link back
  email: [email protected]

On next sync, use externalId to find exact match.


Field Mapping Reference

SalesforceGenesysSync DirectionConflicts
Contact.IdexternalIdSF → GZSF (master)
Contact.FirstNamefirstNameBothSF (master)
Contact.LastNamelastNameBothSF (master)
Contact.EmailemailBothLast-write
Contact.PhonephoneNumbers[0]BothLast-write
Contact.AccountIdorg.externalIdSF → GZSF
Contact.LeadScoreN/ASF → GZN/A
Contact.Tags (custom)attributes.tagsBothGZ wins

Sync Frequency Trade-offs

FrequencyProsConsUse Case
Every 5 minReal-timeHigh API load, costCritical data
Every 15 minNear real-timeMedium loadNormal ops
Every 30 minBalancedSlight delayStandard
HourlyLow cost1hr delayLow priority
DailyVery low cost24hr delayBatch jobs

Recommendation: Start with 30 minutes, adjust based on needs.


Monitoring Sync Health

class SyncMonitor {
  constructor() {
    this.history = [];
  }

  recordSync(result) {
    this.history.push({
      timestamp: new Date(),
      created: result.created,
      updated: result.updated,
      errors: result.errors,
      duration: result.duration,
      conflicts: result.conflicts
    });

    // Alert if too many errors
    if (result.errors.length > 10) {
      this.alertHighErrorRate(result);
    }

    // Alert if sync slow
    if (result.duration > 5 * 60 * 1000) { // 5 minutes
      this.alertSlowSync(result);
    }
  }

  getHealthScore() {
    const recent = this.history.slice(-10); // Last 10 syncs
    const avgErrors = recent.reduce((sum, h) => sum + h.errors.length, 0) / recent.length;
    const avgDuration = recent.reduce((sum, h) => sum + h.duration, 0) / recent.length;

    const score = 100 - (avgErrors * 5) - (avgDuration / 1000); // Deduct for errors and slow
    return Math.max(0, score);
  }
}

When to Use Which Strategy

One-Way Sync (Recommended First)

Use if:

  • ✅ CRM is single source of truth
  • ✅ Only need lookups in Genesys
  • ✅ No updates from agent desktop
  • ✅ Simple, maintainable

Bi-Directional Sync

Use if:

  • ✅ Agents need to update contacts in Genesys
  • ✅ Those updates must sync back to CRM
  • ✅ Can handle complexity
  • ✅ Have conflict resolution strategy

Migration Path

Phase 1: One-Way

  • Sync SF → Genesys (read-only)
  • Test with small set (100 contacts)
  • Verify data accuracy

Phase 2: Expand

  • Sync all contacts
  • Run for 2 weeks, monitor
  • Ensure no data loss

Phase 3: Bi-Directional

  • Add reverse sync (Genesys → SF)
  • Implement conflict resolution
  • Test extensively
  • Monitor for conflicts

Phase 4: Optimize

  • Tune frequency
  • Add caching
  • Optimize performance
  • Monitor cost

  • Chapter 11: Real-World Contact Sync Example
  • Chapter 11: API Endpoints Reference (Contacts API)
  • Chapter 12: Activity Logging (logging interactions back)