Guide
Export Email Data to HubSpot
Your inbox already contains the contacts, companies, and engagement history that HubSpot needs. This guide shows how to extract that data with Nylas CLI and push it into HubSpot CRM — via CSV import for one-time loads or the API v3 for continuous sync.
Why sync email data to HubSpot
Most HubSpot CRM instances suffer from stale data. Sales reps forget to log emails, contacts are created manually with typos, and engagement history is incomplete. Your inbox already has the ground truth — every email is timestamped, every sender has a verified address, and reply patterns reveal actual engagement.
Syncing email data to HubSpot solves three problems at once. First, auto-logged emails eliminate the “did you log that call?” problem — every email interaction is captured as an Engagement object in HubSpot. Second, enriched contacts get real names, email addresses, and company associations from actual email headers instead of manual entry. Third, engagement tracking gives you reply frequency, last-contact dates, and thread counts that power HubSpot lead scoring and workflow triggers.
The Nylas CLI makes the export side trivial. One command gives you structured JSON with sender names, email addresses, timestamps, subject lines, and message bodies. The rest of this guide shows you how to map that data to HubSpot's object model and push it in via CSV or API.
Export data from Nylas CLI
Start by exporting your recent emails and contacts as JSON. These two commands give you everything you need for HubSpot import:
# Export recent emails (sender, subject, date, body snippet)
nylas email list --json --limit 500 > emails.json
# Export contacts (names, emails, phone numbers, companies)
nylas contacts list --json --limit 500 > contacts.jsonTo export a specific email with its full body and headers:
# Read a single email by ID
nylas email read abc123def --json > email-detail.jsonInspect the structure of your exported data to understand what fields are available:
# Preview email fields
cat emails.json | jq '.[0] | keys'
# Typical output: ["bcc","body","cc","date","from","id","labels","snippet","subject","thread_id","to","unread"]
# Preview contact fields
cat contacts.json | jq '.[0] | keys'
# Typical output: ["company_name","email","given_name","id","job_title","phone_numbers","surname"]HubSpot object model mapping
HubSpot organizes CRM data into four core objects: Contacts, Companies, Deals, and Engagements. Here is how your Nylas CLI export maps to each:
| Nylas field | HubSpot object | HubSpot property |
|---|---|---|
from[0].email | Contact | email |
from[0].name (first part) | Contact | firstname |
from[0].name (last part) | Contact | lastname |
from[0].email (domain part) | Company | domain |
phone_numbers[0] | Contact | phone |
subject + body | Engagement | type: EMAIL |
company_name | Company | name |
HubSpot uses email as the unique identifier for Contacts and domain as the unique identifier for Companies. This means you can safely upsert — if a contact already exists, HubSpot updates the existing record instead of creating a duplicate.
CSV import via HubSpot Import tool
For a one-time or occasional import, HubSpot's built-in Import tool accepts CSV files. Use jq to transform your Nylas CLI export into the exact CSV headers HubSpot expects:
# Transform emails.json into HubSpot Contacts CSV
cat emails.json | jq -r '
[.[] | .from[0] | select(.email != null)] | unique_by(.email) |
["Email","First Name","Last Name"] as $header |
($header | @csv),
(.[] | [
.email,
(.name // "" | split(" ")[0] // ""),
(.name // "" | split(" ")[1:] | join(" ") // "")
] | @csv)
' > hubspot-contacts.csv
echo "Created hubspot-contacts.csv with $(wc -l < hubspot-contacts.csv) rows"For Companies, extract unique domains from your email data:
# Transform emails.json into HubSpot Companies CSV
cat emails.json | jq -r '
[.[] | .from[0].email | select(. != null) | split("@")[1]] |
unique |
[.[] | select(. | IN("gmail.com","yahoo.com","outlook.com","hotmail.com") | not)] |
["Company Domain Name"] as $header |
($header | @csv),
(.[] | [.] | @csv)
' > hubspot-companies.csv
echo "Created hubspot-companies.csv with $(wc -l < hubspot-companies.csv) rows"If you have contact data with phone numbers, create a richer CSV:
# Transform contacts.json into HubSpot Contacts CSV with phone and company
cat contacts.json | jq -r '
[.[] | select(.email != null)] | unique_by(.email) |
["Email","First Name","Last Name","Phone Number","Company Name"] as $header |
($header | @csv),
(.[] | [
(.email // ""),
(.given_name // ""),
(.surname // ""),
(.phone_numbers[0] // ""),
(.company_name // "")
] | @csv)
' > hubspot-contacts-full.csvTo import in HubSpot: go to Contacts → Import → Start an import → File from computer. Select the object type (Contacts or Companies), upload your CSV, and map the columns. HubSpot will match by email (Contacts) or domain (Companies) and upsert existing records.
HubSpot API v3 import
For automated or recurring imports, use the HubSpot API v3 directly. You need a private app token from your HubSpot account (Settings → Integrations → Private Apps). The token needs crm.objects.contacts.write and crm.objects.companies.write scopes.
Create or update a contact:
# Create a contact via HubSpot API v3
curl -s -X POST "https://api.hubapi.com/crm/v3/objects/contacts" \
-H "Authorization: Bearer $HUBSPOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"properties": {
"email": "jane@acme.com",
"firstname": "Jane",
"lastname": "Smith",
"phone": "+1-555-0123",
"company": "Acme Corp"
}
}'Create an email engagement and associate it with a contact:
# Create an email engagement via HubSpot API v3
CONTACT_ID="123456"
curl -s -X POST "https://api.hubapi.com/crm/v3/objects/emails" \
-H "Authorization: Bearer $HUBSPOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"properties": {
"hs_timestamp": "2026-03-13T10:00:00.000Z",
"hs_email_direction": "INCOMING_EMAIL",
"hs_email_subject": "Re: Q1 Proposal",
"hs_email_text": "Thanks for sending over the proposal. Let us schedule a call to discuss.",
"hs_email_status": "SENT",
"hs_email_sender_email": "jane@acme.com",
"hs_email_to_email": "you@yourcompany.com"
},
"associations": [
{
"to": { "id": "'"$CONTACT_ID"'" },
"types": [
{ "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 198 }
]
}
]
}'Batch create contacts (up to 100 per request):
# Batch create contacts from Nylas CLI export
cat emails.json | jq '{
"inputs": [
.[:100][] | .from[0] | select(.email != null) |
{
"properties": {
"email": .email,
"firstname": (.name // "" | split(" ")[0] // ""),
"lastname": (.name // "" | split(" ")[1:] | join(" ") // "")
}
}
]
}' | curl -s -X POST "https://api.hubapi.com/crm/v3/objects/contacts/batch/create" \
-H "Authorization: Bearer $HUBSPOT_TOKEN" \
-H "Content-Type: application/json" \
-d @-Associate contacts with companies
HubSpot Contacts and Companies are separate objects. To link them, use the Associations API. This is important because HubSpot uses associations to show which contacts belong to which company in the CRM sidebar.
# First, create or find the company by domain
COMPANY_ID=$(curl -s -X POST "https://api.hubapi.com/crm/v3/objects/companies/search" \
-H "Authorization: Bearer $HUBSPOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"filterGroups": [{
"filters": [{
"propertyName": "domain",
"operator": "EQ",
"value": "acme.com"
}]
}]
}' | jq -r '.results[0].id')
# If no company exists, create one
if [ "$COMPANY_ID" = "null" ] || [ -z "$COMPANY_ID" ]; then
COMPANY_ID=$(curl -s -X POST "https://api.hubapi.com/crm/v3/objects/companies" \
-H "Authorization: Bearer $HUBSPOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"properties": {
"domain": "acme.com",
"name": "Acme Corp"
}
}' | jq -r '.id')
echo "Created company: $COMPANY_ID"
fi
# Associate contact with company (association type 1 = Contact to Company)
CONTACT_ID="123456"
curl -s -X PUT \
"https://api.hubapi.com/crm/v3/objects/contacts/$CONTACT_ID/associations/companies/$COMPANY_ID/1" \
-H "Authorization: Bearer $HUBSPOT_TOKEN"
echo "Associated contact $CONTACT_ID with company $COMPANY_ID"Scheduled sync script
Automate the export-and-import cycle with a cron job. The following script exports new emails since the last sync and pushes contacts and engagements to HubSpot:
#!/usr/bin/env bash
set -euo pipefail
# Configuration
HUBSPOT_TOKEN="${HUBSPOT_TOKEN:?Set HUBSPOT_TOKEN env var}"
SYNC_STATE_FILE="${HOME}/.nylas-hubspot-sync-state"
LOG_FILE="${HOME}/.nylas-hubspot-sync.log"
log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" >> "$LOG_FILE"; }
# Read last sync timestamp (default: 7 days ago)
if [ -f "$SYNC_STATE_FILE" ]; then
LAST_SYNC=$(cat "$SYNC_STATE_FILE")
else
LAST_SYNC=$(date -u -v-7d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ)
fi
log "Starting sync. Last sync: $LAST_SYNC"
# Export recent emails
nylas email list --json --limit 200 > /tmp/hubspot-emails.json
EMAIL_COUNT=$(cat /tmp/hubspot-emails.json | jq 'length')
log "Exported $EMAIL_COUNT emails"
# Extract unique contacts
cat /tmp/hubspot-emails.json | jq '[.[] | .from[0] | select(.email != null)] | unique_by(.email)' > /tmp/hubspot-new-contacts.json
CONTACT_COUNT=$(cat /tmp/hubspot-new-contacts.json | jq 'length')
log "Found $CONTACT_COUNT unique contacts"
# Batch upsert contacts (100 at a time)
cat /tmp/hubspot-new-contacts.json | jq -c '[.[] | {
"properties": {
"email": .email,
"firstname": (.name // "" | split(" ")[0] // ""),
"lastname": (.name // "" | split(" ")[1:] | join(" ") // "")
}
}]' | jq -c '[_nwise(100)][]' | while read -r BATCH; do
RESPONSE=$(echo "{\"inputs\": $BATCH}" | curl -s -X POST \
"https://api.hubapi.com/crm/v3/objects/contacts/batch/create" \
-H "Authorization: Bearer $HUBSPOT_TOKEN" \
-H "Content-Type: application/json" \
-d @-)
CREATED=$(echo "$RESPONSE" | jq '.results | length // 0')
ERRORS=$(echo "$RESPONSE" | jq '.errors | length // 0')
log "Batch: created=$CREATED errors=$ERRORS"
done
# Save sync timestamp
date -u +%Y-%m-%dT%H:%M:%SZ > "$SYNC_STATE_FILE"
log "Sync complete. State saved."
echo "Sync complete. See $LOG_FILE for details."Schedule with cron to run every hour:
# Run every hour
crontab -e
# Add this line:
0 * * * * HUBSPOT_TOKEN="your-token-here" /path/to/hubspot-sync.shFull Python version
The Python version uses the official hubspot-api-client package for type-safe API calls and built-in retry logic. Install it with pip:
pip install hubspot-api-client#!/usr/bin/env python3
"""Export email data from Nylas CLI and sync to HubSpot CRM."""
import json
import subprocess
import sys
from datetime import datetime, timezone
from hubspot import HubSpot
from hubspot.crm.contacts import SimplePublicObjectInputForCreate
from hubspot.crm.companies import SimplePublicObjectInputForCreate as CompanyInput
from hubspot.crm.objects.emails import SimplePublicObjectInputForCreate as EmailInput
from hubspot.crm.associations.v4 import AssociationSpec
def run_nylas(command: list[str]) -> list[dict]:
"""Run a Nylas CLI command and return parsed JSON."""
result = subprocess.run(
["nylas"] + command + ["--json"],
capture_output=True,
text=True,
check=True,
)
return json.loads(result.stdout)
def split_name(full_name: str) -> tuple[str, str]:
"""Split a display name into first and last name."""
parts = full_name.strip().split(" ", 1) if full_name else ["", ""]
return parts[0], parts[1] if len(parts) > 1 else ""
def extract_domain(email_address: str) -> str:
"""Extract the domain from an email address."""
return email_address.split("@")[1] if "@" in email_address else ""
FREEMAIL_DOMAINS = {
"gmail.com", "yahoo.com", "outlook.com", "hotmail.com",
"aol.com", "icloud.com", "mail.com", "protonmail.com",
}
def main():
import os
token = os.environ.get("HUBSPOT_TOKEN")
if not token:
print("Set HUBSPOT_TOKEN environment variable", file=sys.stderr)
sys.exit(1)
api = HubSpot(access_token=token)
# Step 1: Export emails and contacts from Nylas CLI
print("Exporting emails from Nylas CLI...")
emails = run_nylas(["email", "list", "--limit", "200"])
print(f" Exported {len(emails)} emails")
print("Exporting contacts from Nylas CLI...")
contacts = run_nylas(["contacts", "list", "--limit", "200"])
print(f" Exported {len(contacts)} contacts")
# Step 2: Deduplicate senders by email address
seen_emails: set[str] = set()
unique_senders: list[dict] = []
for msg in emails:
sender = msg.get("from", [{}])[0]
addr = sender.get("email", "")
if addr and addr not in seen_emails:
seen_emails.add(addr)
unique_senders.append(sender)
print(f" {len(unique_senders)} unique senders")
# Step 3: Create or update contacts in HubSpot
created, updated, errors = 0, 0, 0
contact_id_map: dict[str, str] = {} # email -> HubSpot contact ID
for sender in unique_senders:
email_addr = sender["email"]
first, last = split_name(sender.get("name", ""))
try:
# Search for existing contact
search_response = api.crm.contacts.search_api.do_search(
public_object_search_request={
"filter_groups": [{
"filters": [{
"property_name": "email",
"operator": "EQ",
"value": email_addr,
}]
}],
"limit": 1,
}
)
if search_response.total > 0:
# Update existing contact
contact_id = search_response.results[0].id
api.crm.contacts.basic_api.update(
contact_id=contact_id,
simple_public_object_input={
"properties": {
"firstname": first,
"lastname": last,
}
},
)
contact_id_map[email_addr] = contact_id
updated += 1
else:
# Create new contact
response = api.crm.contacts.basic_api.create(
simple_public_object_input_for_create=SimplePublicObjectInputForCreate(
properties={
"email": email_addr,
"firstname": first,
"lastname": last,
}
)
)
contact_id_map[email_addr] = response.id
created += 1
except Exception as e:
print(f" Error processing {email_addr}: {e}", file=sys.stderr)
errors += 1
print(f" Contacts: {created} created, {updated} updated, {errors} errors")
# Step 4: Create companies from domains
domains_seen: set[str] = set()
company_id_map: dict[str, str] = {} # domain -> HubSpot company ID
for sender in unique_senders:
domain = extract_domain(sender["email"])
if not domain or domain in FREEMAIL_DOMAINS or domain in domains_seen:
continue
domains_seen.add(domain)
try:
# Search for existing company
search_response = api.crm.companies.search_api.do_search(
public_object_search_request={
"filter_groups": [{
"filters": [{
"property_name": "domain",
"operator": "EQ",
"value": domain,
}]
}],
"limit": 1,
}
)
if search_response.total > 0:
company_id_map[domain] = search_response.results[0].id
else:
response = api.crm.companies.basic_api.create(
simple_public_object_input_for_create=CompanyInput(
properties={
"domain": domain,
"name": domain.split(".")[0].title(),
}
)
)
company_id_map[domain] = response.id
print(f" Created company: {domain} (ID: {response.id})")
except Exception as e:
print(f" Error creating company {domain}: {e}", file=sys.stderr)
print(f" Companies: {len(company_id_map)} total")
# Step 5: Associate contacts with companies
for sender in unique_senders:
email_addr = sender["email"]
domain = extract_domain(email_addr)
contact_id = contact_id_map.get(email_addr)
company_id = company_id_map.get(domain)
if contact_id and company_id:
try:
api.crm.associations.v4.basic_api.create(
object_type="contacts",
object_id=contact_id,
to_object_type="companies",
to_object_id=company_id,
association_spec=[
AssociationSpec(
association_category="HUBSPOT_DEFINED",
association_type_id=1,
)
],
)
except Exception:
pass # association may already exist
print(" Associations created")
# Step 6: Log email engagements
engagement_count = 0
for msg in emails[:100]: # limit to 100 most recent
sender = msg.get("from", [{}])[0]
email_addr = sender.get("email", "")
contact_id = contact_id_map.get(email_addr)
if not contact_id:
continue
try:
ts = msg.get("date", "")
if isinstance(ts, int):
ts = datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()
api.crm.objects.emails.basic_api.create(
simple_public_object_input_for_create=EmailInput(
properties={
"hs_timestamp": ts,
"hs_email_direction": "INCOMING_EMAIL",
"hs_email_subject": msg.get("subject", ""),
"hs_email_text": msg.get("snippet", ""),
"hs_email_status": "SENT",
"hs_email_sender_email": email_addr,
},
associations=[{
"to": {"id": contact_id},
"types": [{
"associationCategory": "HUBSPOT_DEFINED",
"associationTypeId": 198,
}],
}],
)
)
engagement_count += 1
except Exception as e:
print(f" Error logging engagement: {e}", file=sys.stderr)
print(f" Engagements logged: {engagement_count}")
print("Sync complete.")
if __name__ == "__main__":
main()Full TypeScript version
The TypeScript version uses the official @hubspot/api-client package. Install dependencies:
npm install @hubspot/api-clientimport { Client } from "@hubspot/api-client";
import { execFileSync } from "child_process";
const FREEMAIL_DOMAINS = new Set([
"gmail.com", "yahoo.com", "outlook.com", "hotmail.com",
"aol.com", "icloud.com", "mail.com", "protonmail.com",
]);
interface NylasSender {
email: string;
name?: string;
}
interface NylasEmail {
id: string;
from: NylasSender[];
to: NylasSender[];
subject: string;
snippet: string;
date: string | number;
thread_id: string;
}
interface NylasContact {
id: string;
email: string;
given_name?: string;
surname?: string;
company_name?: string;
phone_numbers?: string[];
}
function runNylas(args: string[]): unknown[] {
const result = execFileSync("nylas", [...args, "--json"], {
encoding: "utf-8",
maxBuffer: 50 * 1024 * 1024,
});
return JSON.parse(result);
}
function splitName(name: string | undefined): [string, string] {
if (!name) return ["", ""];
const parts = name.trim().split(" ");
return [parts[0] ?? "", parts.slice(1).join(" ")];
}
function extractDomain(email: string): string {
const at = email.indexOf("@");
return at > -1 ? email.slice(at + 1) : "";
}
async function main() {
const token = process.env.HUBSPOT_TOKEN;
if (!token) {
console.error("Set HUBSPOT_TOKEN environment variable");
process.exit(1);
}
const hubspot = new Client({ accessToken: token });
// Step 1: Export from Nylas CLI
console.log("Exporting emails from Nylas CLI...");
const emails = runNylas(["email", "list", "--limit", "200"]) as NylasEmail[];
console.log(` Exported ${emails.length} emails`);
console.log("Exporting contacts from Nylas CLI...");
const contacts = runNylas(["contacts", "list", "--limit", "200"]) as NylasContact[];
console.log(` Exported ${contacts.length} contacts`);
// Step 2: Deduplicate senders
const seenEmails = new Set<string>();
const uniqueSenders: NylasSender[] = [];
for (const msg of emails) {
const sender = msg.from?.[0];
if (sender?.email && !seenEmails.has(sender.email)) {
seenEmails.add(sender.email);
uniqueSenders.push(sender);
}
}
console.log(` ${uniqueSenders.length} unique senders`);
// Step 3: Create or update contacts
const contactIdMap = new Map<string, string>();
let created = 0;
let updated = 0;
for (const sender of uniqueSenders) {
const [firstname, lastname] = splitName(sender.name);
try {
const search = await hubspot.crm.contacts.searchApi.doSearch({
filterGroups: [{
filters: [{
propertyName: "email",
operator: "EQ",
value: sender.email,
}],
}],
limit: 1,
sorts: [],
properties: [],
after: "0",
});
if (search.total > 0) {
const id = search.results[0].id;
await hubspot.crm.contacts.basicApi.update(id, {
properties: { firstname, lastname },
});
contactIdMap.set(sender.email, id);
updated++;
} else {
const response = await hubspot.crm.contacts.basicApi.create({
properties: { email: sender.email, firstname, lastname },
associations: [],
});
contactIdMap.set(sender.email, response.id);
created++;
}
} catch (err) {
console.error(` Error processing ${sender.email}: ${err}`);
}
}
console.log(` Contacts: ${created} created, ${updated} updated`);
// Step 4: Create companies from domains
const companyIdMap = new Map<string, string>();
const domainsSeen = new Set<string>();
for (const sender of uniqueSenders) {
const domain = extractDomain(sender.email);
if (!domain || FREEMAIL_DOMAINS.has(domain) || domainsSeen.has(domain)) continue;
domainsSeen.add(domain);
try {
const search = await hubspot.crm.companies.searchApi.doSearch({
filterGroups: [{
filters: [{
propertyName: "domain",
operator: "EQ",
value: domain,
}],
}],
limit: 1,
sorts: [],
properties: [],
after: "0",
});
if (search.total > 0) {
companyIdMap.set(domain, search.results[0].id);
} else {
const response = await hubspot.crm.companies.basicApi.create({
properties: {
domain,
name: domain.split(".")[0].charAt(0).toUpperCase() +
domain.split(".")[0].slice(1),
},
associations: [],
});
companyIdMap.set(domain, response.id);
console.log(` Created company: ${domain} (ID: ${response.id})`);
}
} catch (err) {
console.error(` Error creating company ${domain}: ${err}`);
}
}
console.log(` Companies: ${companyIdMap.size} total`);
// Step 5: Associate contacts with companies
for (const sender of uniqueSenders) {
const domain = extractDomain(sender.email);
const contactId = contactIdMap.get(sender.email);
const companyId = companyIdMap.get(domain);
if (contactId && companyId) {
try {
await hubspot.crm.associations.v4.basicApi.create(
"contacts",
contactId,
"companies",
companyId,
[{
associationCategory: "HUBSPOT_DEFINED",
associationTypeId: 1,
}],
);
} catch {
// association may already exist
}
}
}
console.log(" Associations created");
// Step 6: Log email engagements
let engagementCount = 0;
for (const msg of emails.slice(0, 100)) {
const sender = msg.from?.[0];
if (!sender?.email) continue;
const contactId = contactIdMap.get(sender.email);
if (!contactId) continue;
const ts = typeof msg.date === "number"
? new Date(msg.date * 1000).toISOString()
: String(msg.date);
try {
await hubspot.crm.objects.emails.basicApi.create({
properties: {
hs_timestamp: ts,
hs_email_direction: "INCOMING_EMAIL",
hs_email_subject: msg.subject ?? "",
hs_email_text: msg.snippet ?? "",
hs_email_status: "SENT",
hs_email_sender_email: sender.email,
},
associations: [{
to: { id: contactId },
types: [{
associationCategory: "HUBSPOT_DEFINED",
associationTypeId: 198,
}],
}],
});
engagementCount++;
} catch (err) {
console.error(` Error logging engagement: ${err}`);
}
}
console.log(` Engagements logged: ${engagementCount}`);
console.log("Sync complete.");
}
main();Next steps
Now that your email data flows into HubSpot, explore more ways to extract CRM intelligence from your inbox:
- CRM Email Workflows — the full 8-guide series covering extraction, enrichment, and automation.
- Organize Emails by Company — group your inbox by sender domain before exporting.
- Enrich Contact Info from Email — extract job titles, phone numbers, and social links from signatures.
- Build a Contact Hierarchy — infer reporting lines from CC patterns and calendar data.
- Map Organization Contacts — score relationship strength and find warm introductions.
- CLI Command Reference — full list of Nylas CLI commands with flags and examples.