DEV Community

Cover image for Solved: Syncing Salesforce Contacts to Google Contacts (Two-way Sync Script)
Darian Vance
Darian Vance

Posted on • Originally published at wp.me

Solved: Syncing Salesforce Contacts to Google Contacts (Two-way Sync Script)

🚀 Executive Summary

TL;DR: This guide provides a Python-based script for robust two-way synchronization between Salesforce Contacts and Google Contacts. It addresses the problem of siloed data and manual sync inefficiencies by automating the creation and updating of contact records across both platforms.

🎯 Key Takeaways

  • Salesforce API integration utilizes the simple-salesforce library, requiring a Connected App’s Consumer Key/Secret and user credentials (Username, Password, Security Token).
  • Google People API integration uses google-api-python-client and google-auth-oauthlib for OAuth 2.0, storing credentials in a token.pickle file for persistent, non-interactive authentication.
  • Two-way record linking is established using a custom GoogleContactId\_\_c field in Salesforce (storing Google’s resourceName) and embedding the Salesforce Id within a Google Contact’s biography field.

Syncing Salesforce Contacts to Google Contacts (Two-way Sync Script)

As a Senior DevOps Engineer and Technical Writer for ‘TechResolve’, I frequently encounter scenarios where critical business data becomes siloed across various SaaS platforms. Manual data synchronization is not only tedious and prone to human error but also a significant drain on productivity. Imagine the frustration of a sales team member updating a contact’s phone number in Salesforce, only for an executive to call the old number from their Google Contacts. This disconnect leads to inefficiencies, missed opportunities, and a fragmented view of customer relationships.

The problem is clear: Salesforce often serves as the authoritative source for customer data, while Google Contacts is essential for individual productivity and communication within the Google ecosystem. Keeping these two systems aligned, especially with a two-way flow of information, is crucial for seamless operations.

This tutorial provides a comprehensive, step-by-step guide to building a robust Python-based script for two-way synchronization between Salesforce Contacts and Google Contacts. We will cover everything from API setup and authentication to the core synchronization logic, ensuring your contact data remains consistent and up-to-date across both platforms.

Prerequisites

Before diving into the code, ensure you have the following in place:

  • Python 3.x: Installed on your local machine or server.
  • pip: Python’s package installer, usually bundled with Python.
  • Salesforce Developer Account (or Production Org): With API access enabled. You will need:

    • A Salesforce Connected App configured with OAuth settings. Note down the Consumer Key and Consumer Secret.
    • A Salesforce user with API permissions (Username, Password, and Security Token).
    • A custom field on the Contact object in Salesforce (e.g., GoogleContactId__c of type Text) to store the corresponding Google Contact resource name, facilitating linking between records.
  • Google Cloud Project:

    • A Google Cloud Project where you have enabled the Google People API.
    • OAuth 2.0 Client ID credentials (type ‘Desktop app’ for local script execution) downloaded as a credentials.json file. Note down the Client ID and Client Secret.
    • Configured OAuth Consent Screen with appropriate scopes (e.g., https://www.googleapis.com/auth/contacts).
  • Required Python Libraries: Install these using pip:

  pip install simple-salesforce google-api-python-client google-auth-oauthlib google-auth-httplib2
Enter fullscreen mode Exit fullscreen mode

Step-by-Step Guide

Step 1: Salesforce API Setup and Authentication

First, we need to establish a connection to your Salesforce instance. This involves using the simple_salesforce library with your Connected App credentials and user authentication details.

Logic Explanation:

The simple_salesforce library simplifies interactions with the Salesforce REST API. We pass our username, password (concatenated with the security token if it’s not removed via IP whitelisting), consumer key, and consumer secret. Upon successful authentication, an sf object is returned, allowing us to perform queries and DML operations.

Code Snippet:

from simple_salesforce import Salesforce

# --- Salesforce Configuration ---
SF_USERNAME = 'your_sf_username@example.com'
SF_PASSWORD = 'your_sf_password'
SF_SECURITY_TOKEN = 'YOUR_SALESFORCE_SECURITY_TOKEN' # Appended to password if not using IP ranges
SF_CONSUMER_KEY = 'YOUR_CONNECTED_APP_CONSUMER_KEY'
SF_CONSUMER_SECRET = 'YOUR_CONNECTED_APP_CONSUMER_SECRET'

def authenticate_salesforce():
    """Authenticates with Salesforce and returns the SF object."""
    try:
        sf = Salesforce(
            username=SF_USERNAME,
            password=SF_PASSWORD + SF_SECURITY_TOKEN,
            consumer_key=SF_CONSUMER_KEY,
            consumer_secret=SF_CONSUMER_SECRET
        )
        print("Successfully connected to Salesforce!")
        return sf
    except Exception as e:
        print(f"Salesforce connection failed: {e}")
        return None

# Example usage:
# sf_conn = authenticate_salesforce()
# if sf_conn:
#     # You can now use sf_conn for Salesforce API calls
#     pass
Enter fullscreen mode Exit fullscreen mode

Step 2: Google People API Setup and Authentication

Next, we set up authentication for the Google People API. This uses OAuth 2.0, requiring a web-based authorization flow the first time, which then stores a refresh token for subsequent non-interactive use.

Logic Explanation:

The google-auth-oauthlib library handles the OAuth 2.0 flow. The first time you run this, it will open a browser window, ask you to log into your Google account, and grant permission to your application. Upon success, it stores the credentials (including a refresh token) in a token.pickle file. This file allows subsequent runs to authenticate without manual intervention, refreshing the access token as needed.

Code Snippet:

import os
import pickle
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.discovery import build

# --- Google API Configuration ---
SCOPES = ['https://www.googleapis.com/auth/contacts'] # Scope for managing contacts
CREDENTIALS_FILE = 'credentials.json' # Path to your downloaded credentials.json
TOKEN_FILE = 'token.pickle' # File to store token

def authenticate_google():
    """Authenticates with Google People API and returns the service object."""
    creds = None
    # The file token.pickle stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first time.
    if os.path.exists(TOKEN_FILE):
        with open(TOKEN_FILE, 'rb') as token:
            creds = pickle.load(token)
    # If there are no (valid) credentials available, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                CREDENTIALS_FILE, SCOPES)
            creds = flow.run_local_server(port=0)
        # Save the credentials for the next run
        with open(TOKEN_FILE, 'wb') as token:
            pickle.dump(creds, token)

    try:
        service = build('people', 'v1', credentials=creds)
        print("Successfully authenticated with Google People API!")
        return service
    except Exception as e:
        print(f"Google API connection failed: {e}")
        return None

# Example usage:
# google_service = authenticate_google()
# if google_service:
#     # You can now use google_service for Google People API calls
#     pass
Enter fullscreen mode Exit fullscreen mode

Step 3: Develop the Two-Way Sync Logic

This is the core of our solution. We need to fetch contacts from both Salesforce and Google, compare them, and then create or update records as necessary. The key challenge is identifying linked contacts and deciding which system holds the “source of truth” for a given field if a conflict arises.

Logic Explanation:

The synchronization process involves:

  1. Fetching Data: Retrieve all relevant contacts from both Salesforce and Google.
  2. Linking Records: We use the custom GoogleContactId__c field in Salesforce to store the Google Contact’s resourceName. For Google Contacts, we’ll store the Salesforce Contact’s Id in a custom note or biography field.
  3. Comparison & Delta Detection: Iterate through contacts from both systems.
  • If a Salesforce contact has a GoogleContactId__c, we check if the corresponding Google contact exists and has different data. If so, update Google.
  • If a Salesforce contact lacks a GoogleContactId__c, we try to find a matching Google contact (e.g., by email). If found, link them and update Google. If not found, create a new Google contact and link it.
  • Perform the reverse process for Google Contacts: if a Google contact has a linked Salesforce ID, check for updates. If not linked, try to find a match in Salesforce. If not found, create a new Salesforce contact.
    1. Conflict Resolution: For simplicity, this example assumes “last modified wins” or prioritizes Salesforce for certain fields if both are updated. In a real-world scenario, you might have more sophisticated rules.

Code Snippet (Core Sync Functions):

def fetch_salesforce_contacts(sf_conn):
    """Fetches contacts from Salesforce, including the GoogleContactId__c."""
    try:
        # Query contacts, ensure to include the custom GoogleContactId__c field
        sf_contacts_query = sf_conn.query_all(
            "SELECT Id, FirstName, LastName, Email, Phone, MobilePhone, "
            "GoogleContactId__c, LastModifiedDate FROM Contact WHERE IsDeleted = FALSE"
        )
        # Convert to a dictionary for easier lookup
        return {c['Id']: c for c in sf_contacts_query['records']}
    except Exception as e:
        print(f"Error fetching Salesforce contacts: {e}")
        return {}

def fetch_google_contacts(google_service):
    """Fetches contacts from Google, including specific fields."""
    try:
        # Request specific fields to minimize payload and focus on relevant data
        google_contacts_raw = google_service.people().connections().list(
            resourceName='people/me',
            personFields='names,emailAddresses,phoneNumbers,biographies,metadata', # metadata for last modified
            pageSize=1000 # Adjust as needed, max 2000 per page
        ).execute()

        # Extract relevant fields and store Salesforce ID from biography
        contacts = {}
        for gc in google_contacts_raw.get('connections', []):
            sf_id = None
            for bio in gc.get('biographies', []):
                if 'Salesforce ID:' in bio.get('value', ''):
                    sf_id = bio['value'].replace('Salesforce ID:', '').strip()
                    break
            contacts[gc['resourceName']] = {
                'resourceName': gc['resourceName'],
                'etag': gc.get('etag'),
                'names': gc.get('names', []),
                'emailAddresses': gc.get('emailAddresses', []),
                'phoneNumbers': gc.get('phoneNumbers', []),
                'biographies': gc.get('biographies', []),
                'lastModified': gc.get('metadata', {}).get('sources', [{}])[0].get('updateTime'),
                'sf_id': sf_id # Custom field to link
            }
        return contacts
    except Exception as e:
        print(f"Error fetching Google contacts: {e}")
        return {}

def create_salesforce_contact(sf_conn, google_contact_data, google_resource_name):
    """Creates a new contact in Salesforce from Google contact data."""
    try:
        first_name = google_contact_data.get('names', [{}])[0].get('givenName')
        last_name = google_contact_data.get('names', [{}])[0].get('familyName')
        email = google_contact_data.get('emailAddresses', [{}])[0].get('value')

        new_sf_data = {
            'FirstName': first_name,
            'LastName': last_name,
            'Email': email,
            'Phone': google_contact_data.get('phoneNumbers', [{}])[0].get('value'),
            'GoogleContactId__c': google_resource_name # Link back to Google
        }
        result = sf_conn.Contact.create(new_sf_data)
        print(f"Created Salesforce contact {result['id']} for Google contact {google_resource_name}")
        return result['id']
    except Exception as e:
        print(f"Error creating Salesforce contact for {google_resource_name}: {e}")
        return None

def update_salesforce_contact(sf_conn, google_contact_data, sf_contact_existing):
    """Updates an existing Salesforce contact with data from Google."""
    try:
        sf_id = sf_contact_existing['Id']
        # Simple field mapping for demonstration
        updated_sf_data = {
            'FirstName': google_contact_data.get('names', [{}])[0].get('givenName'),
            'LastName': google_contact_data.get('names', [{}])[0].get('familyName'),
            'Email': google_contact_data.get('emailAddresses', [{}])[0].get('value'),
            'Phone': google_contact_data.get('phoneNumbers', [{}])[0].get('value'),
            'GoogleContactId__c': google_contact_data['resourceName'] # Ensure link is present
        }
        sf_conn.Contact.update(sf_id, updated_sf_data)
        print(f"Updated Salesforce contact {sf_id} from Google contact.")
    except Exception as e:
        print(f"Error updating Salesforce contact {sf_contact_existing.get('Id')}: {e}")

def create_google_contact(google_service, sf_contact_data, sf_id):
    """Creates a new contact in Google from Salesforce contact data."""
    try:
        new_google_person = {
            'names': [{'givenName': sf_contact_data.get('FirstName'), 'familyName': sf_contact_data.get('LastName')}],
            'emailAddresses': [{'value': sf_contact_data.get('Email')}],
            'phoneNumbers': [{'value': sf_contact_data.get('Phone'), 'type': 'work'}],
            'biographies': [{'value': f"Salesforce ID: {sf_id}"}] # Link back to Salesforce
        }
        result = google_service.people().createContact(body=new_google_person).execute()
        print(f"Created Google contact {result['resourceName']} for Salesforce contact {sf_id}")
        return result['resourceName']
    except Exception as e:
        print(f"Error creating Google contact for {sf_id}: {e}")
        return None

def update_google_contact(google_service, sf_contact_data, google_contact_existing):
    """Updates an existing Google contact with data from Salesforce."""
    try:
        google_id = google_contact_existing['resourceName']
        etag = google_contact_existing.get('etag') # Required for Google updates

        updated_google_person = {
            'resourceName': google_id,
            'etag': etag,
            'names': [{'givenName': sf_contact_data.get('FirstName'), 'familyName': sf_contact_data.get('LastName')}],
            'emailAddresses': [{'value': sf_contact_data.get('Email')}],
            'phoneNumbers': [{'value': sf_contact_data.get('Phone'), 'type': 'work'}],
            'biographies': [{'value': f"Salesforce ID: {sf_contact_data['Id']}"}] # Ensure link is present
        }

        # Specify fields that are being updated
        update_mask = 'names,emailAddresses,phoneNumbers,biographies'
        google_service.people().updateContact(
            resourceName=google_id,
            updatePersonFields=update_mask,
            body=updated_google_person
        ).execute()
        print(f"Updated Google contact {google_id} from Salesforce contact.")
    except Exception as e:
        print(f"Error updating Google contact {google_contact_existing.get('resourceName')}: {e}")

def run_sync():
    sf_conn = authenticate_salesforce()
    google_service = authenticate_google()

    if not sf_conn or not google_service:
        print("Authentication failed for one or both services. Exiting sync.")
        return

    sf_contacts = fetch_salesforce_contacts(sf_conn)
    google_contacts = fetch_google_contacts(google_service)

    # --- Salesforce to Google Sync ---
    for sf_id, sf_contact in sf_contacts.items():
        google_resource_name = sf_contact.get('GoogleContactId__c')

        if google_resource_name:
            # Salesforce contact is linked to Google
            if google_resource_name in google_contacts:
                # Linked Google contact exists, check if update is needed
                # (You'd implement your comparison logic here, e.g., by LastModifiedDate)
                # For simplicity, let's always update if linked and exists in Google
                update_google_contact(google_service, sf_contact, google_contacts[google_resource_name])
            else:
                # Linked Google contact no longer exists, consider recreating or unlinking
                print(f"Warning: SF contact {sf_id} linked to non-existent Google contact {google_resource_name}. Recreating...")
                new_google_id = create_google_contact(google_service, sf_contact, sf_id)
                if new_google_id:
                    # Update Salesforce with new Google ID
                    sf_conn.Contact.update(sf_id, {'GoogleContactId__c': new_google_id})
        else:
            # Salesforce contact is not linked to Google, try to find a match or create
            found_match = False
            for gc_id, gc in google_contacts.items():
                if gc.get('emailAddresses') and sf_contact.get('Email') and \
                   gc['emailAddresses'][0].get('value') == sf_contact['Email']:
                    # Found by email, link them
                    print(f"Linking SF contact {sf_id} to existing Google contact {gc_id} by email.")
                    sf_conn.Contact.update(sf_id, {'GoogleContactId__c': gc_id})
                    # Update Google contact with SF data
                    update_google_contact(google_service, sf_contact, gc)
                    found_match = True
                    break

            if not found_match:
                # No match found, create new Google contact
                new_google_id = create_google_contact(google_service, sf_contact, sf_id)
                if new_google_id:
                    sf_conn.Contact.update(sf_id, {'GoogleContactId__c': new_google_id})

    # --- Google to Salesforce Sync ---
    for google_resource_name, google_contact in google_contacts.items():
        sf_id = google_contact.get('sf_id')

        if sf_id:
            # Google contact is linked to Salesforce
            if sf_id in sf_contacts:
                # Linked Salesforce contact exists, check if update is needed
                # (Implement your comparison logic here, e.g., by LastModifiedDate)
                # For simplicity, let's always update if linked and exists in Salesforce
                update_salesforce_contact(sf_conn, google_contact, sf_contacts[sf_id])
            else:
                # Linked Salesforce contact no longer exists, consider recreating or unlinking
                print(f"Warning: Google contact {google_resource_name} linked to non-existent SF contact {sf_id}. Recreating...")
                new_sf_id = create_salesforce_contact(sf_conn, google_contact, google_resource_name)
                if new_sf_id:
                    # You would need to update the Google contact's biography with the new SF ID
                    # This is more complex as Google People API requires etag for updates
                    # For simplicity, we'll assume a new Google contact will be created on next SF->Google sync if it's missing SF ID
                    pass # TODO: Implement updating Google contact with new SF ID
        else:
            # Google contact is not linked to Salesforce, try to find a match or create
            found_match = False
            for sfc_id, sfc in sf_contacts.items():
                if sfc.get('Email') and google_contact.get('emailAddresses') and \
                   sfc['Email'] == google_contact['emailAddresses'][0].get('value'):
                    # Found by email, link them
                    print(f"Linking Google contact {google_resource_name} to existing SF contact {sfc_id} by email.")
                    # Update Salesforce contact with Google ID
                    sf_conn.Contact.update(sfc_id, {'GoogleContactId__c': google_resource_name})
                    # Update Salesforce contact with Google data
                    update_salesforce_contact(sf_conn, google_contact, sfc)
                    found_match = True
                    break

            if not found_match:
                # No match found, create new Salesforce contact
                new_sf_id = create_salesforce_contact(sf_conn, google_contact, google_resource_name)
                if new_sf_id:
                    # Update Google contact with new SF ID (requires its etag)
                    # This is left as an exercise for a more robust implementation
                    pass # TODO: Implement updating Google contact with new SF ID

    print("Two-way synchronization complete.")

# To run the sync:
# if __name__ == "__main__":
#     run_sync()
Enter fullscreen mode Exit fullscreen mode

Step 4: Scheduling and Deployment

For continuous synchronization, this script should be run periodically. Common deployment strategies include:

  • Cron Job (Linux/macOS) or Task Scheduler (Windows): Schedule the Python script to run at a desired interval (e.g., every hour).
  0 * * * * /usr/bin/python3 /path/to/your/sync_script.py >> /var/log/salesforce_google_sync.log 2>&1
Enter fullscreen mode Exit fullscreen mode
  • Docker Container: Encapsulate the script and its dependencies in a Docker image for portability and easier management. You can then run the Docker container on a schedule using tools like Kubernetes CronJobs or AWS Fargate/ECS.
  # Example Dockerfile
  FROM python:3.9-slim-buster
  WORKDIR /app
  COPY requirements.txt .
  RUN pip install -r requirements.txt
  COPY . .
  CMD ["python3", "sync_script.py"]
Enter fullscreen mode Exit fullscreen mode
  • Cloud Functions (AWS Lambda, Google Cloud Functions, Azure Functions): For serverless execution, triggered by a schedule (e.g., Cloud Scheduler).

Common Pitfalls

Building integrations often comes with challenges. Here are a few common issues you might encounter:

  • API Rate Limits: Both Salesforce and Google have API limits.

    • Salesforce: Daily API call limits and concurrent request limits. Exceeding these can lead to API_DISABLED_FOR_ORG or REQUEST_LIMIT_EXCEEDED errors. Implement exponential backoff for retries and optimize your queries to fetch only necessary data.
    • Google People API: Limits are per user and per project. Frequent updates or large initial syncs can hit these limits, resulting in 429 Resource exhausted errors. Again, implement backoff strategies.
  • Authentication and Authorization Errors:

    • Salesforce: Incorrect username/password, expired security token, IP restrictions, or insufficient user permissions can cause INVALID_GRANT or INSUFFICIENT_ACCESS_OR_READ_ONLY errors. Ensure the Connected App has correct OAuth scopes and the user profile has “API Enabled” permission.
    • Google: Incorrect credentials.json, expired refresh token, or incorrect OAuth scopes (e.g., not requesting https://www.googleapis.com/auth/contacts) can lead to invalid_grant or insufficient_scope errors. Always re-run the authenticate_google() function if token.pickle seems stale or corrupted.
  • Data Mapping Mismatches: Different platforms handle fields differently (e.g., phone types, address structures, custom fields). Inaccurate mapping can lead to data loss or incorrect data. Carefully define how each Salesforce field maps to a Google Contact field and vice versa, especially for complex fields like addresses or custom attributes.

  • Duplicate Contacts and Infinite Loops: A poorly designed sync can create endless duplicates or trigger an infinite loop of updates between systems. The use of a linking ID (GoogleContactId__c in Salesforce, and Salesforce ID in Google’s notes) is critical to prevent this. During initial sync, robust matching logic (e.g., exact email match) is vital before creating new records.

Conclusion

Automating the synchronization of Salesforce Contacts with Google Contacts transforms a manual chore into a seamless, efficient process. By following this tutorial, you’ve built a Python script that provides a foundation for two-way data flow, ensuring that your sales, marketing, and operational teams always have access to the most current contact information, regardless of which platform they are using.

This solution not only eliminates the drudgery and error associated with manual data entry but also enhances overall productivity and data integrity across your organization. As a DevOps engineer, consider this a stepping stone towards a more integrated and automated enterprise ecosystem.

Next Steps:

  • Robust Error Handling and Logging: Implement comprehensive try-except blocks and detailed logging to track sync operations, identify failures, and aid debugging.
  • Advanced Field Mapping: Extend the mapping logic to include more complex fields like addresses, company names, and custom fields, potentially using a configuration file (e.g., YAML) for easier management.
  • Delta Sync by Last Modified Date: Instead of fetching all contacts every time, implement logic to only retrieve and process contacts that have been modified since the last successful sync, improving efficiency.
  • Webhook-based Real-time Sync: For critical, near real-time updates, explore Salesforce Platform Events or Google Cloud Pub/Sub with webhooks to trigger your sync script on specific contact changes.
  • Configuration Management: Externalize sensitive credentials and configuration parameters into environment variables or a secure configuration file (e.g., using Python-dotenv or AWS Secrets Manager).

Darian Vance

👉 Read the original article on TechResolve.blog


☕ Support my work

If this article helped you, you can buy me a coffee:

👉 https://buymeacoffee.com/darianvance

Top comments (0)