Guide
Export Email Data to Zoho CRM
Zoho CRM is one of the most popular CRMs for small and mid-size teams, but keeping it in sync with your actual email conversations is manual work. This guide shows how to extract email and contact data from the Nylas CLI and push it into Zoho CRM — via CSV import for one-off loads or the REST API v2 for automated syncs.
Why sync email data to Zoho CRM
Zoho CRM is affordable, flexible, and widely adopted — but it only knows what you tell it. Every sales conversation, introduction, and follow-up lives in your inbox, and manually copying that context into CRM records is tedious and error-prone. Most teams stop doing it after a few weeks, and their CRM becomes stale.
Automating the sync solves three problems. First, auto-enrich leads — every email address, name, and company domain you interact with becomes a lead or contact in Zoho without manual entry. Second, connect email conversations to CRM records — email subjects, timestamps, and thread context get logged as Notes on the matching Contact or Lead, so anyone on the team can see the full history. Third, keep Accounts current — company domains extracted from email addresses map directly to Zoho Account records, and the sync can create or update them automatically.
The Nylas CLI makes the extraction side trivial. A single --json flag gives you structured data from any email provider — Gmail, Outlook, Exchange, IMAP — and you can pipe it directly into a script that talks to the Zoho CRM API.
Export data from Nylas CLI
Start by pulling your recent email and contact data. The --json flag returns structured output that includes sender names, email addresses, subjects, and timestamps.
# Export recent emails as JSON
nylas email list --json --limit 200 > emails.json
# Export contacts as JSON
nylas contacts list --json --limit 200 > contacts.json
# Preview the email data structure
cat emails.json | jq '.[0] | {from: .from, subject: .subject, date: .date}'The email JSON includes arrays of sender/recipient objects with name and email fields. The contacts JSON includes full names, email addresses, phone numbers, and company names when available. Both are the raw material for CRM import.
# Extract unique senders with names and domains
cat emails.json | jq '[.[] | {
email: .from[0].email,
name: .from[0].name,
domain: (.from[0].email | split("@")[1]),
last_subject: .subject,
last_date: .date
}] | unique_by(.email) | sort_by(.domain)'Zoho CRM object model mapping
Zoho CRM organizes data into modules: Leads, Contacts, Accounts, Deals, and Notes. Mapping email data to these modules correctly is the key to a clean import. Here is how each field maps.
# Email field → Zoho CRM field mapping
#
# Email address → Contact.Email or Lead.Email
# First name → Contact.First_Name or Lead.First_Name
# Last name → Contact.Last_Name or Lead.Last_Name
# Company domain → Account.Account_Name (or Account.Website)
# Phone number → Contact.Phone or Lead.Phone
# Email activity → Note.Note_Content (linked to Contact/Lead)
#
# Zoho CRM modules used:
# Lead — new contacts you haven't qualified yet
# Contact — qualified contacts associated with an Account
# Account — the company (one Account has many Contacts)
# Deal — an opportunity linked to an Account + Contact
# Note — free-text record attached to any module recordThe decision between Lead and Contact depends on your workflow. If you treat every new email sender as a potential prospect, create them as Leads and convert to Contacts after qualification. If you already know the sender is a real business contact, create them directly as a Contact linked to an Account.
# Transform email data into Zoho-ready JSON
cat emails.json | jq '[.[] | {
Email: .from[0].email,
First_Name: (.from[0].name | split(" ")[0] // ""),
Last_Name: (.from[0].name | split(" ")[1:] | join(" ") // "(Unknown)"),
Company: (.from[0].email | split("@")[1] | split(".")[0]
| (.[:1] | ascii_upcase) + .[1:]),
Lead_Source: "Email",
Description: ("Last email: " + .subject + " on " + .date)
}] | unique_by(.Email)'CSV import via Zoho CRM Import tool
The fastest way to do a one-off import is Zoho CRM’s built-in Import tool. Convert your JSON to CSV, then upload through the Zoho CRM web UI. This works well for initial data loads or periodic bulk updates.
# Convert emails JSON to Zoho-ready CSV
cat emails.json | jq -r '
["Email","First_Name","Last_Name","Company","Lead_Source","Description"],
(.[] | [
.from[0].email,
(.from[0].name | split(" ")[0] // ""),
(.from[0].name | split(" ")[1:] | join(" ") // "(Unknown)"),
(.from[0].email | split("@")[1] | split(".")[0]
| (.[:1] | ascii_upcase) + .[1:]),
"Email",
("Last email: " + .subject)
]) | @csv' | sort -t',' -k1,1 -u > zoho-leads.csv
# Preview the CSV
head -5 zoho-leads.csvTo import in Zoho CRM: go to Setup → Data Administration → Import, select the Leads or Contacts module, upload your CSV, map columns to Zoho fields, and choose Skip or Overwrite for duplicate handling. Use the Email field as the duplicate-check key.
# Convert contacts JSON to Zoho-ready CSV
cat contacts.json | jq -r '
["Email","First_Name","Last_Name","Phone","Company"],
(.[] | select(.emails[0].email != null) | [
.emails[0].email,
(.given_name // ""),
(.surname // "(Unknown)"),
(.phone_numbers[0].number // ""),
(.company_name // "")
]) | @csv' > zoho-contacts.csvZoho CRM REST API v2 import
For automated workflows, use the Zoho CRM REST API v2. All endpoints live under https://www.zohoapis.com/crm/v2/. Authentication uses OAuth 2.0 Bearer tokens. You’ll need a refresh token from the Zoho API Console (Self Client grant type), a client ID, and a client secret.
# Zoho CRM API v2 — key endpoints
#
# POST /crm/v2/Leads — create or upsert Leads
# POST /crm/v2/Contacts — create or upsert Contacts
# POST /crm/v2/Accounts — create or upsert Accounts
# POST /crm/v2/Notes — create Notes linked to a record
# POST /crm/v2/Leads/upsert — insert or update, deduplicate by field
#
# Auth header: Authorization: Zoho-oauthtoken {access_token}
#
# Token refresh endpoint:
# POST https://accounts.zoho.com/oauth/v2/token
# grant_type=refresh_token
# client_id={client_id}
# client_secret={client_secret}
# refresh_token={refresh_token}Create a Lead via the API with a simple POST. The request body wraps records in a data array, and you can send up to 100 records per request.
# Create a Lead via Zoho CRM API v2
curl -X POST "https://www.zohoapis.com/crm/v2/Leads" \
-H "Authorization: Zoho-oauthtoken YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": [
{
"Email": "sarah@acme.com",
"First_Name": "Sarah",
"Last_Name": "Chen",
"Company": "Acme Corp",
"Lead_Source": "Email",
"Phone": "+1-555-0142",
"Description": "Imported from email data via Nylas CLI"
}
]
}'To log email activity as a Note attached to a Lead or Contact, use the Notes endpoint with a Parent_Id and se_module that references the parent record.
# Attach an email log as a Note to a Lead
curl -X POST "https://www.zohoapis.com/crm/v2/Notes" \
-H "Authorization: Zoho-oauthtoken YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": [
{
"Note_Title": "Email: Re: Q2 Partnership Proposal",
"Note_Content": "From: sarah@acme.com\nDate: 2026-03-12\nSubject: Re: Q2 Partnership Proposal\n\nDiscussed pricing tiers and timeline for integration.",
"Parent_Id": "5678901234567890",
"se_module": "Leads"
}
]
}'Upsert to avoid duplicates
The biggest problem with CRM imports is duplicates. Zoho CRM’s upsert endpoint solves this by checking a unique field before inserting. Use Email as the duplicate-check field — if a Lead or Contact with that email already exists, the API updates the existing record instead of creating a new one.
# Upsert Leads — insert new, update existing by Email
curl -X POST "https://www.zohoapis.com/crm/v2/Leads/upsert" \
-H "Authorization: Zoho-oauthtoken YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": [
{
"Email": "sarah@acme.com",
"First_Name": "Sarah",
"Last_Name": "Chen",
"Company": "Acme Corp",
"Lead_Source": "Email",
"Description": "Updated from email sync on 2026-03-13"
}
],
"duplicate_check_fields": ["Email"]
}'The same pattern works for Contacts and Accounts. For Accounts, use Website or Account_Name as the duplicate-check field.
# Upsert Contacts by Email
curl -X POST "https://www.zohoapis.com/crm/v2/Contacts/upsert" \
-H "Authorization: Zoho-oauthtoken YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": [
{
"Email": "sarah@acme.com",
"First_Name": "Sarah",
"Last_Name": "Chen",
"Phone": "+1-555-0142",
"Account_Name": "Acme Corp"
}
],
"duplicate_check_fields": ["Email"]
}'
# Upsert Accounts by Website
curl -X POST "https://www.zohoapis.com/crm/v2/Accounts/upsert" \
-H "Authorization: Zoho-oauthtoken YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": [
{
"Account_Name": "Acme Corp",
"Website": "https://acme.com"
}
],
"duplicate_check_fields": ["Website"]
}'Scheduled sync script
A Bash script that runs on a cron schedule handles the end-to-end flow: export from Nylas CLI, refresh the Zoho OAuth token, and upsert records. Run it every hour or every day depending on how fresh your CRM data needs to be.
#!/usr/bin/env bash
# sync-to-zoho.sh — Export Nylas email data to Zoho CRM
# Usage: ./sync-to-zoho.sh
# Schedule: crontab -e → 0 * * * * /path/to/sync-to-zoho.sh
set -euo pipefail
# --- Configuration (use environment variables) ---
ZOHO_CLIENT_ID="${ZOHO_CLIENT_ID:?Set ZOHO_CLIENT_ID}"
ZOHO_CLIENT_SECRET="${ZOHO_CLIENT_SECRET:?Set ZOHO_CLIENT_SECRET}"
ZOHO_REFRESH_TOKEN="${ZOHO_REFRESH_TOKEN:?Set ZOHO_REFRESH_TOKEN}"
ZOHO_API_BASE="https://www.zohoapis.com/crm/v2"
ZOHO_AUTH_URL="https://accounts.zoho.com/oauth/v2/token"
LIMIT=50
# --- Step 1: Refresh OAuth access token ---
echo "[$(date)] Refreshing Zoho access token..."
TOKEN_RESPONSE=$(curl -s -X POST "$ZOHO_AUTH_URL" \
-d "grant_type=refresh_token" \
-d "client_id=$ZOHO_CLIENT_ID" \
-d "client_secret=$ZOHO_CLIENT_SECRET" \
-d "refresh_token=$ZOHO_REFRESH_TOKEN")
ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')
if [ "$ACCESS_TOKEN" = "null" ] || [ -z "$ACCESS_TOKEN" ]; then
echo "ERROR: Failed to refresh token" >&2
exit 1
fi
# --- Step 2: Export emails from Nylas CLI ---
echo "[$(date)] Exporting emails..."
nylas email list --json --limit "$LIMIT" > /tmp/nylas-emails.json
# --- Step 3: Transform to Zoho Lead records ---
LEADS=$(cat /tmp/nylas-emails.json | jq '[.[] | {
Email: .from[0].email,
First_Name: (.from[0].name | split(" ")[0] // ""),
Last_Name: (.from[0].name | split(" ")[1:] | join(" ") // "(Unknown)"),
Company: (.from[0].email | split("@")[1] | split(".")[0]
| (.[:1] | ascii_upcase) + .[1:]),
Lead_Source: "Email",
Description: ("Last email: " + .subject)
}] | unique_by(.Email)')
TOTAL=$(echo "$LEADS" | jq 'length')
echo "[$(date)] Found $TOTAL unique leads to sync"
# --- Step 4: Upsert in batches of 100 ---
BATCH_SIZE=100
for ((i = 0; i < TOTAL; i += BATCH_SIZE)); do
BATCH=$(echo "$LEADS" | jq ".[$i:$((i + BATCH_SIZE))]")
PAYLOAD=$(jq -n --argjson data "$BATCH" '{
data: $data,
duplicate_check_fields: ["Email"]
}')
RESPONSE=$(curl -s -X POST "$ZOHO_API_BASE/Leads/upsert" \
-H "Authorization: Zoho-oauthtoken $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
CREATED=$(echo "$RESPONSE" | jq '[.data[] | select(.status == "success" and .action == "insert")] | length')
UPDATED=$(echo "$RESPONSE" | jq '[.data[] | select(.status == "success" and .action == "update")] | length')
echo "[$(date)] Batch $((i / BATCH_SIZE + 1)): $CREATED created, $UPDATED updated"
done
echo "[$(date)] Sync complete"Full Python version with OAuth refresh
A production-ready Python script that handles token refresh, batched upserts, and email activity logging as Notes. Uses requests for HTTP and reads credentials from environment variables.
#!/usr/bin/env python3
"""Sync Nylas email data to Zoho CRM.
Usage:
export ZOHO_CLIENT_ID="your_client_id"
export ZOHO_CLIENT_SECRET="your_client_secret"
export ZOHO_REFRESH_TOKEN="your_refresh_token"
python sync_zoho.py
"""
import json
import os
import subprocess
import sys
from datetime import datetime
import requests
ZOHO_AUTH_URL = "https://accounts.zoho.com/oauth/v2/token"
ZOHO_API_BASE = "https://www.zohoapis.com/crm/v2"
BATCH_SIZE = 100
def refresh_access_token() -> str:
"""Exchange a refresh token for a new access token."""
resp = requests.post(ZOHO_AUTH_URL, data={
"grant_type": "refresh_token",
"client_id": os.environ["ZOHO_CLIENT_ID"],
"client_secret": os.environ["ZOHO_CLIENT_SECRET"],
"refresh_token": os.environ["ZOHO_REFRESH_TOKEN"],
})
resp.raise_for_status()
token = resp.json().get("access_token")
if not token:
raise RuntimeError(f"Token refresh failed: {resp.text}")
return token
def run_cli(*args: str) -> str:
"""Run a Nylas CLI command and return stdout."""
result = subprocess.run(
["nylas", *args],
capture_output=True, text=True, check=True,
)
return result.stdout
def export_emails(limit: int = 200) -> list[dict]:
"""Export emails from Nylas CLI as structured dicts."""
raw = run_cli("email", "list", "--json", "--limit", str(limit))
return json.loads(raw)
def export_contacts(limit: int = 200) -> list[dict]:
"""Export contacts from Nylas CLI as structured dicts."""
raw = run_cli("contacts", "list", "--json", "--limit", str(limit))
return json.loads(raw)
def email_to_lead(email: dict) -> dict:
"""Transform a Nylas email object into a Zoho Lead record."""
sender = email.get("from", [{}])[0]
addr = sender.get("email", "")
name = sender.get("name", "")
parts = name.split(" ", 1)
domain = addr.split("@")[1] if "@" in addr else ""
company = domain.split(".")[0].capitalize() if domain else ""
return {
"Email": addr,
"First_Name": parts[0] if parts else "",
"Last_Name": parts[1] if len(parts) > 1 else "(Unknown)",
"Company": company,
"Lead_Source": "Email",
"Description": f"Last email: {email.get('subject', '(no subject)')}",
}
def contact_to_zoho(contact: dict) -> dict | None:
"""Transform a Nylas contact into a Zoho Contact record."""
emails = contact.get("emails", [])
if not emails:
return None
addr = emails[0].get("email", "")
phones = contact.get("phone_numbers", [])
return {
"Email": addr,
"First_Name": contact.get("given_name", ""),
"Last_Name": contact.get("surname", "(Unknown)"),
"Phone": phones[0]["number"] if phones else "",
"Account_Name": contact.get("company_name", ""),
}
def email_to_note(email: dict, parent_id: str, module: str = "Leads") -> dict:
"""Transform an email into a Zoho Note linked to a parent record."""
sender = email.get("from", [{}])[0]
return {
"Note_Title": f"Email: {email.get('subject', '(no subject)')}",
"Note_Content": (
f"From: {sender.get('email', '')}
"
f"Date: {email.get('date', '')}
"
f"Subject: {email.get('subject', '')}"
),
"Parent_Id": parent_id,
"se_module": module,
}
def upsert_records(
token: str,
module: str,
records: list[dict],
duplicate_field: str = "Email",
) -> dict:
"""Upsert records to Zoho CRM in batches."""
headers = {
"Authorization": f"Zoho-oauthtoken {token}",
"Content-Type": "application/json",
}
stats = {"created": 0, "updated": 0, "errors": 0}
for i in range(0, len(records), BATCH_SIZE):
batch = records[i : i + BATCH_SIZE]
payload = {
"data": batch,
"duplicate_check_fields": [duplicate_field],
}
resp = requests.post(
f"{ZOHO_API_BASE}/{module}/upsert",
headers=headers,
json=payload,
)
resp.raise_for_status()
for item in resp.json().get("data", []):
if item.get("status") == "success":
if item.get("action") == "insert":
stats["created"] += 1
else:
stats["updated"] += 1
else:
stats["errors"] += 1
return stats
def create_notes(token: str, notes: list[dict]) -> int:
"""Create Notes in Zoho CRM. Returns count of successes."""
headers = {
"Authorization": f"Zoho-oauthtoken {token}",
"Content-Type": "application/json",
}
created = 0
for i in range(0, len(notes), BATCH_SIZE):
batch = notes[i : i + BATCH_SIZE]
resp = requests.post(
f"{ZOHO_API_BASE}/Notes",
headers=headers,
json={"data": batch},
)
resp.raise_for_status()
created += sum(
1 for item in resp.json().get("data", [])
if item.get("status") == "success"
)
return created
def main() -> None:
for var in ("ZOHO_CLIENT_ID", "ZOHO_CLIENT_SECRET", "ZOHO_REFRESH_TOKEN"):
if var not in os.environ:
print(f"ERROR: {var} not set", file=sys.stderr)
sys.exit(1)
now = datetime.now().isoformat(timespec="seconds")
print(f"[{now}] Starting Zoho CRM sync...")
# Step 1: Refresh token
token = refresh_access_token()
print(f"[{now}] Access token refreshed")
# Step 2: Export from Nylas CLI
emails = export_emails(limit=200)
contacts = export_contacts(limit=200)
print(f"[{now}] Exported {len(emails)} emails, {len(contacts)} contacts")
# Step 3: Transform and deduplicate leads
leads_by_email: dict[str, dict] = {}
for email in emails:
lead = email_to_lead(email)
if lead["Email"] and lead["Email"] not in leads_by_email:
leads_by_email[lead["Email"]] = lead
leads = list(leads_by_email.values())
print(f"[{now}] {len(leads)} unique leads to upsert")
# Step 4: Upsert leads
lead_stats = upsert_records(token, "Leads", leads)
print(
f"[{now}] Leads: {lead_stats['created']} created, "
f"{lead_stats['updated']} updated, {lead_stats['errors']} errors"
)
# Step 5: Transform and upsert contacts
zoho_contacts = [
c for c in (contact_to_zoho(ct) for ct in contacts)
if c is not None and c["Email"]
]
if zoho_contacts:
contact_stats = upsert_records(token, "Contacts", zoho_contacts)
print(
f"[{now}] Contacts: {contact_stats['created']} created, "
f"{contact_stats['updated']} updated, {contact_stats['errors']} errors"
)
print(f"[{now}] Sync complete")
if __name__ == "__main__":
main()Full TypeScript version with OAuth refresh
The same workflow in TypeScript using the built-in fetch API (Node 18+). Handles token refresh, batched upserts, and Note creation.
#!/usr/bin/env npx tsx
/**
* Sync Nylas email data to Zoho CRM.
*
* Usage:
* export ZOHO_CLIENT_ID="your_client_id"
* export ZOHO_CLIENT_SECRET="your_client_secret"
* export ZOHO_REFRESH_TOKEN="your_refresh_token"
* npx tsx sync_zoho.ts
*/
import { execFileSync } from "child_process";
const ZOHO_AUTH_URL = "https://accounts.zoho.com/oauth/v2/token";
const ZOHO_API_BASE = "https://www.zohoapis.com/crm/v2";
const BATCH_SIZE = 100;
interface ZohoLead {
Email: string;
First_Name: string;
Last_Name: string;
Company: string;
Lead_Source: string;
Description: string;
}
interface ZohoContact {
Email: string;
First_Name: string;
Last_Name: string;
Phone: string;
Account_Name: string;
}
interface ZohoNote {
Note_Title: string;
Note_Content: string;
Parent_Id: string;
se_module: string;
}
interface UpsertStats {
created: number;
updated: number;
errors: number;
}
function requireEnv(name: string): string {
const val = process.env[name];
if (!val) {
console.error(`ERROR: ${name} not set`);
process.exit(1);
}
return val;
}
function runCli(...args: string[]): string {
return execFileSync("nylas", args, { encoding: "utf-8" });
}
async function refreshAccessToken(): Promise<string> {
const params = new URLSearchParams({
grant_type: "refresh_token",
client_id: requireEnv("ZOHO_CLIENT_ID"),
client_secret: requireEnv("ZOHO_CLIENT_SECRET"),
refresh_token: requireEnv("ZOHO_REFRESH_TOKEN"),
});
const resp = await fetch(ZOHO_AUTH_URL, {
method: "POST",
body: params,
});
if (!resp.ok) {
throw new Error(`Token refresh failed: ${resp.status} ${await resp.text()}`);
}
const data = (await resp.json()) as { access_token?: string };
if (!data.access_token) {
throw new Error("No access_token in response");
}
return data.access_token;
}
function emailToLead(email: Record<string, unknown>): ZohoLead {
const from = (email.from as Array<{ email: string; name: string }>)?.[0] ?? {};
const addr = from.email ?? "";
const name = from.name ?? "";
const parts = name.split(" ");
const domain = addr.includes("@") ? addr.split("@")[1] : "";
const company = domain
? domain.split(".")[0].charAt(0).toUpperCase() + domain.split(".")[0].slice(1)
: "";
return {
Email: addr,
First_Name: parts[0] ?? "",
Last_Name: parts.slice(1).join(" ") || "(Unknown)",
Company: company,
Lead_Source: "Email",
Description: `Last email: ${(email.subject as string) ?? "(no subject)"}`,
};
}
function contactToZoho(
contact: Record<string, unknown>,
): ZohoContact | null {
const emails = contact.emails as Array<{ email: string }> | undefined;
if (!emails?.length) return null;
const phones = contact.phone_numbers as Array<{ number: string }> | undefined;
return {
Email: emails[0].email,
First_Name: (contact.given_name as string) ?? "",
Last_Name: (contact.surname as string) || "(Unknown)",
Phone: phones?.[0]?.number ?? "",
Account_Name: (contact.company_name as string) ?? "",
};
}
async function upsertRecords(
token: string,
module: string,
records: Record<string, unknown>[],
duplicateField = "Email",
): Promise<UpsertStats> {
const headers = {
Authorization: `Zoho-oauthtoken ${token}`,
"Content-Type": "application/json",
};
const stats: UpsertStats = { created: 0, updated: 0, errors: 0 };
for (let i = 0; i < records.length; i += BATCH_SIZE) {
const batch = records.slice(i, i + BATCH_SIZE);
const resp = await fetch(`${ZOHO_API_BASE}/${module}/upsert`, {
method: "POST",
headers,
body: JSON.stringify({
data: batch,
duplicate_check_fields: [duplicateField],
}),
});
resp.ok || console.error(`Upsert error: ${resp.status}`);
const body = (await resp.json()) as {
data?: Array<{ status: string; action: string }>;
};
for (const item of body.data ?? []) {
if (item.status === "success") {
item.action === "insert" ? stats.created++ : stats.updated++;
} else {
stats.errors++;
}
}
}
return stats;
}
async function createNotes(
token: string,
notes: ZohoNote[],
): Promise<number> {
const headers = {
Authorization: `Zoho-oauthtoken ${token}`,
"Content-Type": "application/json",
};
let created = 0;
for (let i = 0; i < notes.length; i += BATCH_SIZE) {
const batch = notes.slice(i, i + BATCH_SIZE);
const resp = await fetch(`${ZOHO_API_BASE}/Notes`, {
method: "POST",
headers,
body: JSON.stringify({ data: batch }),
});
const body = (await resp.json()) as {
data?: Array<{ status: string }>;
};
created += (body.data ?? []).filter((d) => d.status === "success").length;
}
return created;
}
async function main(): Promise<void> {
const now = new Date().toISOString();
console.log(`[${now}] Starting Zoho CRM sync...`);
// Step 1: Refresh token
const token = await refreshAccessToken();
console.log(`[${now}] Access token refreshed`);
// Step 2: Export from Nylas CLI
const emails: Record<string, unknown>[] = JSON.parse(
runCli("email", "list", "--json", "--limit", "200"),
);
const contacts: Record<string, unknown>[] = JSON.parse(
runCli("contacts", "list", "--json", "--limit", "200"),
);
console.log(`[${now}] Exported ${emails.length} emails, ${contacts.length} contacts`);
// Step 3: Transform and deduplicate leads
const leadsByEmail = new Map<string, ZohoLead>();
for (const email of emails) {
const lead = emailToLead(email);
if (lead.Email && !leadsByEmail.has(lead.Email)) {
leadsByEmail.set(lead.Email, lead);
}
}
const leads = [...leadsByEmail.values()];
console.log(`[${now}] ${leads.length} unique leads to upsert`);
// Step 4: Upsert leads
const leadStats = await upsertRecords(token, "Leads", leads);
console.log(
`[${now}] Leads: ${leadStats.created} created, ${leadStats.updated} updated, ${leadStats.errors} errors`,
);
// Step 5: Transform and upsert contacts
const zohoContacts = contacts
.map(contactToZoho)
.filter((c): c is ZohoContact => c !== null && c.Email !== "");
if (zohoContacts.length > 0) {
const contactStats = await upsertRecords(token, "Contacts", zohoContacts);
console.log(
`[${now}] Contacts: ${contactStats.created} created, ${contactStats.updated} updated, ${contactStats.errors} errors`,
);
}
console.log(`[${now}] Sync complete`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});Next steps
- Enrich Contact and Company Info from Email — extract job titles, phone numbers, and LinkedIn URLs from email signatures before pushing to Zoho CRM for richer Lead and Contact records.
- Organize Emails by Company and Domain — group your inbox by sender domain to identify which companies to create as Zoho Accounts and prioritize your CRM import.
- Personalize Outbound Email from the CLI — once your contacts are in Zoho CRM, use enriched data to send personalized follow-ups at scale.
- CRM Email Workflows — the full series hub with all 8 guides covering extraction, organization, enrichment, and automated outbound.