Guide
Export Email Data to Pipedrive
Pipedrive organizes everything around deals. A deal moves through pipeline stages, and every deal connects to a Person and Organization. This guide exports email data from Gmail, Outlook, Exchange, Yahoo, iCloud, and IMAP using Nylas CLI and maps it to Pipedrive's deal-centric data model — covering the Activities API, custom fields, Lead vs Deal workflows, and Organization linking.
By Hazik
Pipedrive's email sync vs. CLI import
Pipedrive has a built-in email sync feature. You connect your Gmail or Outlook account, and Pipedrive automatically links incoming and outgoing emails to matching Persons by email address. It's convenient for daily use, but it has limits that matter for data-driven teams.
Pipedrive's email sync only works with Gmail and Outlook (Microsoft 365). If your team uses Exchange on-prem, Yahoo, iCloud, or IMAP, Pipedrive can't sync those inboxes. According to Pipedrive's documentation, “Email Sync currently supports Google and Microsoft email accounts.”
The sync also processes emails individually as they arrive. There's no way to bulk-import 6 months of email history. And you can't customize how email data maps to Pipedrive fields. Subject lines become activity subjects, but thread depth, CC patterns, and response times get lost.
A CLI-based import solves all three. Nylas CLI works with any provider. You can export your entire email archive at once. And you control exactly which fields get mapped where, including custom fields.
Export from Nylas CLI
Pull email and contact data into local JSON, then preview the fields that matter for Pipedrive's deal-centric model: sender frequency, subject lines, and dates help determine which contacts deserve a Deal.
# Export recent emails and contacts as JSON
nylas email list --json --limit 500 > emails.json
nylas contacts list --json --limit 500 > contacts.json
# Preview deal-relevant email fields (subject, sender, date)
cat emails.json | jq '[.[:5][] | {
from: .from[0].email,
subject: .subject,
date: .date,
thread_id: .thread_id
}]'Pipedrive's deal-centric data model
Unlike Salesforce or HubSpot, Pipedrive is deal-centric. Everything revolves around the visual pipeline. Contacts exist to support deals, not the other way around. This affects how you structure your import.
The object hierarchy in Pipedrive:
- Organization — a company. Has a name, address, and custom fields. Persons belong to Organizations.
- Person — an individual. Has a name, email (array), phone (array), and is linked to an Organization. Persons are the primary actors in deals.
- Lead — a pre-qualified prospect. Lives in the Leads Inbox, separate from the pipeline. Convert to a Deal when qualified. Leads were added in 2020.
- Deal — an opportunity. Sits in a pipeline stage. Links to a Person (primary contact) and an Organization. Has a value, currency, and expected close date.
- Activity — a logged event (email, call, meeting, task). Links to a Person, Organization, and/or Deal. Has a type, subject, due date, and done flag.
The import order matters: create Organizations first, then Persons (linking to Organizations), then optionally create Deals, then log Activities (linking to Persons and Deals).
Lead vs Deal: when to use which
Pipedrive added Leads in 2020 to separate unqualified prospects from active deals. Before Leads existed, teams dumped everyone into the first pipeline stage, cluttering the visual board.
For email import, here's the decision tree:
- Unknown sender, first contact → create as a Lead. They stay in the Leads Inbox until someone qualifies them.
- Known contact with an active opportunity → create as a Person + Deal. The Deal enters your pipeline at the appropriate stage.
- Existing Person, new email thread about a new topic → create a new Deal linked to the existing Person.
# Create a Lead from an unknown email sender
curl -s -X POST "https://api.pipedrive.com/v1/leads?api_token=$PIPEDRIVE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Sarah Chen - Acme Corp",
"person_id": null,
"organization_id": null,
"value": {"amount": 0, "currency": "USD"},
"expected_close_date": null,
"visible_to": "3",
"was_seen": false
}' | jq '{id: .data.id, title: .data.title}'
# Convert a Lead to a Deal (moves it into the pipeline)
curl -s -X POST "https://api.pipedrive.com/v1/leads/$LEAD_ID/convert?api_token=$PIPEDRIVE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"deal": {
"title": "Acme Corp - Q3 Engagement",
"value": 25000,
"currency": "USD",
"stage_id": 1
}
}' | jq '{deal_id: .data.deal_id}'Custom fields and field mapping
Standard Pipedrive fields cover name, email, and phone. But email data contains richer signals: thread count, response time, CC count, last contact date. Custom fields let you store these on Person, Organization, or Deal records.
Create custom fields in Settings → Company settings → Data fields. Each field gets an API key like abc123_thread_count. List your fields via the API to find their keys:
# List all Person custom fields and their API keys
curl -s "https://api.pipedrive.com/v1/personFields?api_token=$PIPEDRIVE_TOKEN" \
| jq '.data[] | {name, key, field_type}'
# Create a custom field for "Email Thread Count"
curl -s -X POST "https://api.pipedrive.com/v1/personFields?api_token=$PIPEDRIVE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Email Thread Count",
"field_type": "double"
}' | jq '{key: .data.key, name: .data.name}'
# Create a Person with custom field values
# Replace the hash keys with your actual field keys
THREAD_COUNT_KEY="abc123_thread_count"
LAST_EMAIL_KEY="def456_last_email_date"
curl -s -X POST "https://api.pipedrive.com/v1/persons?api_token=$PIPEDRIVE_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"Sarah Chen\",
\"email\": [{\"value\": \"sarah@acme.com\", \"primary\": true, \"label\": \"work\"}],
\"phone\": [{\"value\": \"+1-555-0142\", \"primary\": true, \"label\": \"work\"}],
\"org_id\": 123,
\"$THREAD_COUNT_KEY\": 7,
\"$LAST_EMAIL_KEY\": \"2026-03-12\"
}" | jq '{id: .data.id, name: .data.name}'Activities API for email logging
The Activities API is where email data gets logged. Each Activity has a type (Pipedrive supports custom types, but email is built in), a subject, a due date, and links to Person/Organization/Deal.
Key fields for email activities:
type: "email"— marks it as an email activity (appears with the email icon)done: 1— marks it as completed (it already happened)person_id— links to the sender/recipient Persondeal_id— links to an active Deal (shows email on the deal timeline)note— HTML-formatted body content (Pipedrive renders HTML in activity notes)
# Log an email activity with HTML note
curl -s -X POST "https://api.pipedrive.com/v1/activities?api_token=$PIPEDRIVE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subject": "Re: Q3 proposal follow-up",
"type": "email",
"due_date": "2026-03-10",
"due_time": "14:30",
"duration": "00:05",
"person_id": 456,
"org_id": 123,
"deal_id": 789,
"done": 1,
"note": "<p>From: <b>sarah@acme.com</b></p><p>Discussed pricing tiers and timeline for Q3 rollout. Sarah confirmed budget approval from their CFO.</p>"
}' | jq '{id: .data.id, subject: .data.subject}'
# Batch-log multiple email activities from Nylas export
cat emails.json | jq -c '.[:50][]' | while read -r msg; do
SUBJECT=$(echo "$msg" | jq -r '.subject // "No subject"')
FROM_EMAIL=$(echo "$msg" | jq -r '.from[0].email // empty')
DATE=$(echo "$msg" | jq -r 'if .date | type == "number" then (.date | todate | split("T")[0]) else (.date | split("T")[0]) end')
[ -z "$FROM_EMAIL" ] && continue
# Find Person by email
PERSON_ID=$(curl -s "https://api.pipedrive.com/v1/persons/search?term=$FROM_EMAIL&fields=email&api_token=$PIPEDRIVE_TOKEN" \
| jq -r '.data.items[0].item.id // empty')
[ -z "$PERSON_ID" ] && continue
curl -s -X POST "https://api.pipedrive.com/v1/activities?api_token=$PIPEDRIVE_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg subj "$SUBJECT" --arg date "$DATE" --argjson pid "$PERSON_ID" '{
subject: $subj,
type: "email",
due_date: $date,
person_id: $pid,
done: 1
}')" > /dev/null
echo "Logged: $SUBJECT -> Person $PERSON_ID"
donePipedrive Marketplace and Zapier alternatives
Pipedrive's Marketplace has several email-to-CRM integrations. The most popular are Mailchimp Pipedrive Integration, Outfunnel, and Zapier connectors. These work for simple use cases, but they have limitations:
- Zapier charges per task (a “Zap”). At $0.01-0.03/task, syncing 500 emails/day costs $150-450/month. A CLI script runs for free.
- Outfunnel starts at $29/month for 1,000 contacts and doesn't support custom field mapping. It pushes contacts but can't create Deals or log Activities with custom metadata.
- Marketplace integrations use Pipedrive's OAuth scope, so they can only access data you authorize. A private API token gives you full CRUD access to all objects.
For developer teams, the CLI-to-API approach is cheaper, more flexible, and gives you audit trails. You own the code, control the mapping, and can add custom business logic (like auto-creating Deals for contacts from specific domains).
Python sync script
A complete Python script that exports from Nylas CLI, creates Organizations and Persons with custom fields, logs email Activities, and optionally creates Deals for high-engagement contacts.
#!/usr/bin/env python3
"""Sync Nylas CLI email data to Pipedrive CRM."""
import json
import subprocess
import os
import sys
from collections import Counter
import requests
BASE = "https://api.pipedrive.com/v1"
TOKEN = os.environ.get("PIPEDRIVE_API_TOKEN", "")
if not TOKEN:
print("Set PIPEDRIVE_API_TOKEN", file=sys.stderr)
sys.exit(1)
def api(path: str, **params: str) -> str:
params["api_token"] = TOKEN
qs = "&".join(f"{k}={v}" for k, v in params.items())
return f"{BASE}/{path}?{qs}"
# Export from Nylas CLI
emails = json.loads(subprocess.run(
["nylas", "email", "list", "--json", "--limit", "500"],
capture_output=True, text=True, check=True,
).stdout)
contacts = json.loads(subprocess.run(
["nylas", "contacts", "list", "--json", "--limit", "500"],
capture_output=True, text=True, check=True,
).stdout)
print(f"Exported {len(emails)} emails, {len(contacts)} contacts")
# Count emails per sender (for engagement scoring)
sender_counts: Counter[str] = Counter()
for msg in emails:
addr = (msg.get("from") or [{}])[0].get("email", "")
if addr:
sender_counts[addr] += 1
# Phase 1: Create Organizations from unique domains
FREEMAIL = {"gmail.com", "yahoo.com", "outlook.com", "hotmail.com"}
org_map: dict[str, int] = {} # domain -> org_id
domains = set()
for msg in emails:
addr = (msg.get("from") or [{}])[0].get("email", "")
if "@" in addr:
domain = addr.split("@")[1]
if domain not in FREEMAIL:
domains.add(domain)
for domain in domains:
name = domain.split(".")[0].title()
# Search first
resp = requests.get(api("organizations/search", term=name))
items = resp.json().get("data", {}).get("items", [])
if items:
org_map[domain] = items[0]["item"]["id"]
else:
resp = requests.post(api("organizations"), json={"name": name})
org_map[domain] = resp.json()["data"]["id"]
print(f" Created org: {name}")
print(f"Mapped {len(org_map)} organizations")
# Phase 2: Create Persons with engagement metadata
person_map: dict[str, int] = {} # email -> person_id
seen: set[str] = set()
for msg in emails:
sender = (msg.get("from") or [{}])[0]
addr = sender.get("email", "")
if not addr or addr in seen:
continue
seen.add(addr)
# Search for existing Person
resp = requests.get(api("persons/search", term=addr, fields="email"))
items = resp.json().get("data", {}).get("items", [])
if items:
person_map[addr] = items[0]["item"]["id"]
continue
name = sender.get("name", "") or addr.split("@")[0]
domain = addr.split("@")[1] if "@" in addr else ""
org_id = org_map.get(domain)
payload: dict = {
"name": name,
"email": [{"value": addr, "primary": True, "label": "work"}],
}
if org_id:
payload["org_id"] = org_id
resp = requests.post(api("persons"), json=payload)
if resp.ok:
person_map[addr] = resp.json()["data"]["id"]
print(f"Mapped {len(person_map)} persons")
# Phase 3: Auto-create Deals for high-engagement contacts
# Contacts with 5+ emails get a Deal in the pipeline
DEAL_THRESHOLD = 5
deal_count = 0
for addr, count in sender_counts.items():
if count >= DEAL_THRESHOLD and addr in person_map:
domain = addr.split("@")[1] if "@" in addr else ""
org_id = org_map.get(domain)
name_part = domain.split(".")[0].title() if domain else "Unknown"
payload: dict = {
"title": f"{name_part} - Email Engagement",
"person_id": person_map[addr],
"stage_id": 1, # First pipeline stage
}
if org_id:
payload["org_id"] = org_id
resp = requests.post(api("deals"), json=payload)
if resp.ok:
deal_count += 1
print(f"Created {deal_count} deals for high-engagement contacts")
# Phase 4: Log email Activities
activity_count = 0
for msg in emails:
sender = (msg.get("from") or [{}])[0]
addr = sender.get("email", "")
person_id = person_map.get(addr)
if not person_id:
continue
date_val = msg.get("date", 0)
if isinstance(date_val, (int, float)) and date_val > 0:
from datetime import datetime, timezone
due_date = datetime.fromtimestamp(date_val, tz=timezone.utc).strftime("%Y-%m-%d")
else:
continue
payload = {
"subject": (msg.get("subject") or "No subject")[:150],
"type": "email",
"due_date": due_date,
"person_id": person_id,
"done": 1,
}
domain = addr.split("@")[1] if "@" in addr else ""
org_id = org_map.get(domain)
if org_id:
payload["org_id"] = org_id
resp = requests.post(api("activities"), json=payload)
if resp.ok:
activity_count += 1
print(f"Logged {activity_count} activities")
print("Sync complete.")Install and run:
pip install requests
export PIPEDRIVE_API_TOKEN="your_api_token_here"
python3 sync_pipedrive.pyNext steps
- Export email data to Salesforce — Bulk API 2.0, SOQL queries, and Apex triggers
- Export email data to HubSpot — batch endpoints, auto-company creation, and timeline events
- Organize emails by company — group your inbox by sender domain before importing
- Enrich contacts from email — extract job titles and phone numbers from signatures for richer Person records
- CRM Email Workflows series — all 8 guides for extracting CRM intelligence from your inbox
- Full command reference — every Nylas CLI flag and subcommand documented