A CRM is only as good as the contact data inside it, and the richest source of that data is usually a user’s own Google address book. This recipe pulls Google Contacts through the Nylas Contacts API, pushes them into an external CRM such as Salesforce or HubSpot, keeps the two sides current as people change, and deduplicates against records that already exist in the CRM by matching on email address.
The fetch step is intentionally thin here. If you only need the raw read call and its filters, see How to list Google contacts. This page picks up where that one ends: the write side, the ongoing sync, and the matching logic that stops you creating duplicate CRM records.
How do I push Google Contacts into a CRM?
Section titled “How do I push Google Contacts into a CRM?”Read the address book once with GET /v3/grants/{grant_id}/contacts, map each contact object to your CRM’s contact shape, then create or update the record in the CRM. Nylas returns a single contact schema for every provider, so one mapping function serves Google, Outlook, and Exchange grants without branching.
The full sync is a four-stage pipeline: pull, dedupe, map, push. The pull is a single API call that costs 1 request; the other three stages are your own code against the CRM’s REST API. Keep the stages independent so a mapping bug on one record never blocks the rest of the batch. A typical first run for a single Google account moves between 200 and 2,000 contacts, small enough to process in one pass without parallelism.
The snippet below reads one grant’s contacts and walks the result. Map and push happen inside the loop.
import requests
NYLAS_API = "https://api.us.nylas.com"headers = {"Authorization": "Bearer <NYLAS_API_KEY>"}
resp = requests.get( f"{NYLAS_API}/v3/grants/<NYLAS_GRANT_ID>/contacts", headers=headers, params={"limit": 200, "source": "address_book"}, # skip auto-collected inbox contacts)crm_emails = {e.strip().lower() for e in crm.get_all_emails()} # one batched readfor contact in resp.json()["data"]: primary = next((e["email"] for e in (contact.get("emails") or [])), None) if not primary: continue upsert_into_crm(primary, contact, crm_emails) # dedupe + map + push, shown belowHow do I deduplicate against existing CRM records?
Section titled “How do I deduplicate against existing CRM records?”Match on the normalized primary email address, because it is the one field both Google Contacts and every CRM agree on. Lowercase and trim each address before comparing, look up the CRM for a record with that email, then update it if found or create it if not. This single rule keeps a re-run from doubling your contact count.
Email is the right key for one practical reason: names drift and phone numbers are often blank, but a person’s primary work address stays stable. Around 70 percent of duplicate CRM contacts trace back to the same email stored under two slightly different names, so collapsing on email removes most of them in one move. Build a lookup of existing CRM emails before the loop rather than querying the CRM once per contact, which turns 1,000 individual lookups into a single batched read.
The function below normalizes the email, checks an in-memory set of CRM emails, and upserts accordingly.
def upsert_into_crm(email, contact, crm_emails): key = email.strip().lower() record = { "first_name": contact.get("given_name", ""), "last_name": contact.get("surname", ""), "email": key, "company": contact.get("company_name", ""), "title": contact.get("job_title", ""), } if key in crm_emails: crm.update_contact(key, record) # existing record, no duplicate else: crm.create_contact(record) crm_emails.add(key)Most CRM APIs expose a native upsert keyed on an external ID, which is safer than your own lookup under concurrency. Use it when available and fall back to the email set only when it is not.
How do I keep contacts in sync over time?
Section titled “How do I keep contacts in sync over time?”Subscribe a webhook to the contact.updated and contact.deleted triggers, the only two contact events Nylas emits. Your endpoint receives a notification when a contact changes or is removed, so you mirror that one change into the CRM instead of re-reading the whole address book on a timer. There is no contact.created trigger, so treat a brand-new contact as a contact.updated event.
One Google-specific detail shapes the timing. Google has no push channel for contacts, so Nylas polls Google accounts roughly every 5 minutes, and a webhook for a changed Google contact fires after that interval rather than instantly. That delay is fine for CRM hygiene, where a few minutes of lag never matters. The webhook setup itself lives in Get real-time updates with webhooks.
The handler below reuses the same dedupe path so webhook-driven and full syncs stay consistent.
def handle_webhook(event): trigger = event["type"] contact = event["data"]["object"] primary = next((e["email"] for e in (contact.get("emails") or [])), None) if not primary: return if trigger == "contact.deleted": crm.archive_contact(primary.strip().lower()) else: # contact.updated crm_emails = {e.strip().lower() for e in crm.get_all_emails()} upsert_into_crm(primary, contact, crm_emails)How do I page through a large address book?
Section titled “How do I page through a large address book?”Pass a limit and follow the next_cursor value Nylas returns until it is absent. A single list response holds up to 200 contacts, and the default page size is 30, so set limit=200 for a bulk sync to cut the number of round trips. Hand the cursor back through the page_token query parameter to fetch the next page.
A full sync should page to the end before it pushes anything, so a mid-run failure never leaves the CRM half-updated. Cursor paging is stable across the run, which matters when an address book holds tens of thousands of entries. For an account with 10,000 contacts, a page size of 200 means 50 requests rather than the 334 you would make at the default of 30. The pagination mechanics apply to every Nylas list endpoint and are documented in the Contacts API reference.
The loop below collects every page before the dedupe stage runs.
def fetch_all(grant_id): cursor, contacts = None, [] while True: params = {"limit": 200} if cursor: params["page_token"] = cursor page = requests.get( f"{NYLAS_API}/v3/grants/{grant_id}/contacts", headers=headers, params=params, ).json() contacts.extend(page["data"]) cursor = page.get("next_cursor") if not cursor: return contactsThings to know about Google to CRM sync
Section titled “Things to know about Google to CRM sync”Google’s contact model has a few traits that change how a CRM sync behaves, and planning for them up front saves a messy first run. The points below cover sources, blank fields, sync lag, and the direction of truth.
Google splits contacts into saved entries and auto-created suggestions, exposed through the source field as address_book and inbox. A heavy email user can carry thousands of inbox entries against a few hundred saved ones, and most of those auto-created rows do not belong in a CRM. Filter on source=address_book so you only sync contacts the user actually saved. Expect blank fields too: roughly 40 percent of Google contacts have no company or title, so guard your mapping against missing keys rather than assuming every field is present.
Decide which side is the source of truth before you turn on two-way updates. If the CRM is authoritative, let contact.updated events enrich but never overwrite a CRM-edited field. The first run almost always surfaces edge cases like shared inboxes and vendor addresses, so review the batch before pointing it at a live CRM. For the field-by-field provider limits, see List and sync contacts from Google and Outlook.
What’s next
Section titled “What’s next”- How to list Google contacts for the raw read call, filters, and OAuth scopes
- List and sync contacts from Google and Outlook for the cross-provider sync and webhook setup
- Sync email contacts to a CRM for the email-derived flow that builds contacts from message participants
- Get real-time updates with webhooks to wire up the
contact.updatedtrigger - Contacts API reference for every endpoint, parameter, and field