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 any email provider 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.
Written by Hazik Director of Product Management
Reviewed by Hazik
Pipedrive's email sync vs. CLI import
Pipedrive's built-in email sync connects Gmail or Outlook accounts and automatically links messages to matching Persons by email address. This native sync covers 2 of the 6 major email providers, leaving Exchange on-prem, Yahoo, iCloud, and IMAP users without a sync path. A CLI-based import removes that provider restriction and adds bulk-import and custom field mapping.
According to Pipedrive's documentation, “Email Sync currently supports Google and Microsoft email accounts.” Teams on other providers have no native option. Nylas CLI normalizes email data across all 6 providers into a single JSON format, so the same import script works regardless of the source mailbox.
The native 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 gaps. 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.
Shape data for Pipedrive's deal model
Pipedrive organizes everything around Deals, so you need to shape email data before import. Sender frequency indicates deal potential, subject lines suggest pipeline stage, and dates establish recency. Contacts with 5 or more email threads in the last 90 days are strong candidates for automatic Deal creation rather than Person-only records.
The Nylas CLI email list command returns up to 500 messages per call in JSON format. Each message includes from, subject, date, and thread_id fields. The contacts list command returns structured contact records with names, email addresses, and phone numbers. Together, these two exports provide the raw data for Pipedrive's Person, Organization, and Activity objects.
# Pull email history and contacts
nylas email list --json --limit 500 > emails.json
nylas contacts list --json --limit 500 > contacts.json
# Pipedrive-specific: preview deal-relevant fields
# Frequency and subject lines determine which contacts get Deals vs just Persons
cat emails.json | jq '[.[:5][] | {
from: .from[0].email,
subject: .subject,
date: .date,
thread_id: .thread_id
}]'Pipedrive's deal-centric data model
Pipedrive's data model is deal-centric, meaning every object exists to support a Deal moving through the pipeline. Unlike Salesforce (which centers on Accounts) or HubSpot (which centers on Contacts), Pipedrive treats Persons and Organizations as supporting entities. Pipedrive reports over 100,000 paying customers as of 2024, and this deal-first architecture is the defining difference for teams importing email data.
The object hierarchy in Pipedrive has 5 core types, and import order matters:
- 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 Leads and Deals serve different stages of the sales funnel. Leads live in the Leads Inbox, a separate space from the pipeline, and hold unqualified prospects until someone reviews them. Deals sit in pipeline stages and represent active opportunities with an expected value and close date. Pipedrive added Leads in 2020, and teams that use them report 30-40% less clutter in their pipeline boards.
For email import, the sender's engagement history determines which object to create:
- 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.
The Pipedrive REST API exposes separate endpoints for Leads (/v1/leads) and Deals (/v1/deals). Creating a Lead requires only a title field, while converting a Lead to a Deal uses the /v1/leads/:id/convert endpoint. The conversion preserves the Person and Organization links, so you don't lose attribution data during the transition.
# 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
Pipedrive custom fields extend the standard Person, Organization, and Deal schemas with additional data points extracted from email. Standard Pipedrive fields cover name, email, and phone, but email data contains richer signals like thread count, average response time, CC frequency, and last contact date. Pipedrive supports 11 custom field types including double (numeric), varchar (text), date, set (multi-select), and enum (single-select).
Custom fields are created in Settings, then Company settings, then Data fields. Each field gets a unique API key like abc123_thread_count. The /v1/personFields endpoint lists all fields and their keys, which you need when constructing Person payloads with custom data.
# 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
Pipedrive's Activities API logs events (emails, calls, meetings, tasks) against Person, Organization, and Deal records, creating a timeline of interactions visible on each deal's detail page. The /v1/activities endpoint accepts up to 100 requests per 2-second window on the Professional plan. Each Activity links to a Person and optionally to an Organization and Deal, so a single email log entry can appear on all three record timelines simultaneously.
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)
The first example creates a single Activity with an HTML-formatted note. The second example batch-processes a Nylas CLI email export, searching for each sender's Person record by email address and logging one Activity per message. The batch script processes up to 50 emails per run to stay within Pipedrive's rate limits.
# 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 lists over 400 integrations, including several email-to-CRM connectors like Mailchimp, Outfunnel, and Zapier. These tools work for simple sync scenarios where you want contacts pushed into Pipedrive without writing code. For teams that need custom field mapping, Deal auto-creation rules, or multi-provider support, the CLI-to-API approach is cheaper and more flexible.
- 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
The Python script below runs a 4-phase sync: export emails and contacts via Nylas CLI, create Organizations from unique sender domains, create Persons linked to those Organizations, then log Activities and auto-create Deals for contacts with 5 or more email threads. A typical run processing 500 emails completes in under 60 seconds, making approximately 1 API call per unique sender plus 1 per email message.
The script uses the requests library for Pipedrive API calls and subprocess to invoke Nylas CLI. It deduplicates senders by email address and skips free-mail domains (gmail.com, yahoo.com, outlook.com, hotmail.com) when creating Organizations, since those domains don't map to real companies. Each phase prints a summary count so you can verify the import without checking Pipedrive's UI.
#!/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.")The script requires the requests library (installable via pip) and a Pipedrive API token stored in the PIPEDRIVE_API_TOKEN environment variable. Generate an API token from Pipedrive's Settings under Personal Preferences, then API. The token grants full CRUD access to all objects in your Pipedrive account.
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
- Pipedrive Developer Docs: API v1 reference — canonical Persons, Organizations, Deals, and Activities endpoints
- Pipedrive: OAuth 2.0 authorization — required token and scope flow for production integrations
- Pipedrive: Rate limiting — token-based budgeting that determines how aggressively you can batch