đ 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-salesforcelibrary, requiring a Connected Appâs Consumer Key/Secret and user credentials (Username, Password, Security Token). - Google People API integration uses
google-api-python-clientandgoogle-auth-oauthlibfor OAuth 2.0, storing credentials in atoken.picklefile for persistent, non-interactive authentication. - Two-way record linking is established using a custom
GoogleContactId\_\_cfield in Salesforce (storing GoogleâsresourceName) and embedding the SalesforceIdwithin 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__cof 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.jsonfile. 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
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
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
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:
- Fetching Data: Retrieve all relevant contacts from both Salesforce and Google.
-
Linking Records: We use the custom
GoogleContactId__cfield in Salesforce to store the Google ContactâsresourceName. For Google Contacts, weâll store the Salesforce ContactâsIdin a custom note or biography field. - 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.
- 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()
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
- 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"]
- 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_ORGorREQUEST_LIMIT_EXCEEDEDerrors. 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 exhaustederrors. Again, implement backoff strategies.
-
Salesforce: Daily API call limits and concurrent request limits. Exceeding these can lead to
-
Authentication and Authorization Errors:
-
Salesforce: Incorrect username/password, expired security token, IP restrictions, or insufficient user permissions can cause
INVALID_GRANTorINSUFFICIENT_ACCESS_OR_READ_ONLYerrors. 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 requestinghttps://www.googleapis.com/auth/contacts) can lead toinvalid_grantorinsufficient_scopeerrors. Always re-run theauthenticate_google()function iftoken.pickleseems stale or corrupted.
-
Salesforce: Incorrect username/password, expired security token, IP restrictions, or insufficient user permissions can cause
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__cin 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).
đ Read the original article on TechResolve.blog
â Support my work
If this article helped you, you can buy me a coffee:

Top comments (0)