Guide
Export Email Data to HubSpot
HubSpot auto-creates companies from email domains, triggers workflows on engagement properties, and deduplicates by email address. This guide exports data from Gmail, Outlook, Exchange, Yahoo, iCloud, and IMAP using Nylas CLI and pushes it into HubSpot via batch API endpoints, the association API, and timeline events.
By Prem Keshari
HubSpot's native email integration vs. CLI sync
HubSpot offers built-in email integration for Gmail and Outlook. You connect your inbox in Settings → General → Email, and HubSpot logs emails automatically. It works, but it has three limitations that matter for engineering teams.
First, it only supports Gmail and Outlook. If your team uses Exchange on-prem, Yahoo, iCloud, or custom IMAP servers, HubSpot's native integration doesn't cover them. According to HubSpot's documentation, “email logging requires a connected Gmail or Office 365 email account.”
Second, it logs emails from individual inboxes. There's no way to bulk-import historical email data. If you have 2 years of email history with 500 contacts, you can't retroactively log those interactions. A CLI-based approach can process your entire email archive in one run.
Third, you can't customize the mapping. HubSpot decides which fields get populated. With a CLI export, you control exactly which properties get set, which custom fields get populated, and which records get created vs. updated.
Export from Nylas CLI
Pull email and contact data into local JSON files. Before pushing to HubSpot, filter out freemail domains (gmail.com, yahoo.com) to avoid creating junk Company records from auto-association.
# Export recent emails and contacts as JSON
nylas email list --json --limit 500 > emails.json
nylas contacts list --json --limit 500 > contacts.json
# Filter out freemail senders that would create junk Companies
cat emails.json | jq '[.[] | select(
.from[0].email | split("@")[1] |
IN("gmail.com","yahoo.com","outlook.com","hotmail.com") | not
)]' > emails-business.jsonAutomatic company creation from domains
HubSpot has a feature most CRMs lack: when you create a Contact with an email address, HubSpot automatically creates a Company from the email domain and links the two. Create a contact with sarah@acme.com, and HubSpot creates a Company with domain: acme.com and associates them.
This is controlled by the setting “Create and associate companies with contacts” under Settings → Objects → Companies. It's on by default. HubSpot also enriches the Company record with data from its own database: employee count, revenue, industry, and address. According to HubSpot, their company database covers over 20 million businesses.
This means your import strategy can be simpler. You don't need to create Companies explicitly. Just batch-create Contacts with email addresses, and HubSpot handles the rest. But there are gotchas:
- Free email domains (gmail.com, yahoo.com) create junk Company records. Filter them before import.
- Auto-created Companies use the domain as the name (e.g., “acme.com”). If you want “Acme Corp”, create the Company first with an explicit name.
- The auto-association uses association type 1 (primary company). A Contact can have multiple Company associations, but only one primary.
Free vs. paid tier API limits
HubSpot's API limits vary significantly by plan. This matters when you're importing thousands of records.
| Plan | Per 10 seconds | Per day | Batch size |
|---|---|---|---|
| Free / Starter | 100 calls | 250,000 calls | 100 records |
| Professional | 150 calls | 500,000 calls | 100 records |
| Enterprise | 200 calls | 500,000 calls | 100 records |
The batch endpoints accept up to 100 records per request. Importing 500 contacts takes 5 API calls instead of 500. Always use batch endpoints when importing more than a handful of records.
Batch create and update via API v3
HubSpot's batch API v3 endpoints let you create or update up to 100 records in one request. The key endpoints:
POST /crm/v3/objects/contacts/batch/create— create up to 100 contactsPOST /crm/v3/objects/contacts/batch/update— update up to 100 contacts by IDPOST /crm/v3/objects/contacts/batch/upsert— create or update by email (requires Pro+)POST /crm/v3/objects/emails/batch/create— log up to 100 email engagements
# Batch create contacts from Nylas CLI export
# Transform emails.json senders into HubSpot batch payload
cat emails.json | jq '{
inputs: [
[.[] | .from[0] | select(.email != null)]
| unique_by(.email)
| .[:100][]
| {
properties: {
email: .email,
firstname: (.name // "" | split(" ")[0]),
lastname: (.name // "" | split(" ")[1:] | join(" "))
}
}
]
}' > /tmp/hubspot-batch.json
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 @/tmp/hubspot-batch.json | jq '{
status: .status,
created: (.results | length),
errors: (.errors // [] | length)
}'Timeline events API
Beyond standard email engagements, HubSpot's Timeline Events API lets you create custom timeline entries on any CRM record. This is useful when you want to log email metadata that doesn't fit into the standard engagement model.
First, create a timeline event template in your HubSpot app (Settings → Integrations → Private Apps → Timeline Events). Define the tokens (fields) your events will contain. Then POST events:
# Create a custom timeline event on a Contact
curl -s -X POST "https://api.hubapi.com/crm/v3/timeline/events" \
-H "Authorization: Bearer $HUBSPOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"eventTemplateId": "123456",
"objectId": "901",
"timestamp": "2026-03-12T14:30:00Z",
"tokens": {
"emailSubject": "Re: Q3 Partnership Proposal",
"senderDomain": "acme.com",
"threadLength": "7",
"responseTimeMinutes": "23",
"ccCount": "3"
}
}'Timeline events appear in the Contact's activity feed alongside calls, meetings, and standard emails. The custom tokens let you track metrics HubSpot doesn't capture natively: thread length, CC count, response time, attachment count, etc.
Association API deep dive
HubSpot's Association API v4 connects objects to each other. The association type ID determines the relationship. The common ones for email import:
| From | To | Type ID | Label |
|---|---|---|---|
| Contact | Company | 1 | Primary company |
| Contact | Deal | 3 | Contact to deal |
| Contact | 198 | Email to contact | |
| Company | Deal | 5 | Company to deal |
# Associate a Contact with a Company (type 1)
curl -s -X PUT \
"https://api.hubapi.com/crm/v4/objects/contacts/$CONTACT_ID/associations/companies/$COMPANY_ID" \
-H "Authorization: Bearer $HUBSPOT_TOKEN" \
-H "Content-Type: application/json" \
-d '[{
"associationCategory": "HUBSPOT_DEFINED",
"associationTypeId": 1
}]'
# Associate an email engagement with a Contact (type 198)
curl -s -X PUT \
"https://api.hubapi.com/crm/v4/objects/emails/$EMAIL_ID/associations/contacts/$CONTACT_ID" \
-H "Authorization: Bearer $HUBSPOT_TOKEN" \
-H "Content-Type: application/json" \
-d '[{
"associationCategory": "HUBSPOT_DEFINED",
"associationTypeId": 198
}]'
# Batch associate — link multiple contacts to a deal at once
curl -s -X POST \
"https://api.hubapi.com/crm/v4/associations/contacts/deals/batch/create" \
-H "Authorization: Bearer $HUBSPOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"inputs": [
{
"from": {"id": "101"},
"to": {"id": "201"},
"types": [{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 3}]
},
{
"from": {"id": "102"},
"to": {"id": "201"},
"types": [{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 3}]
}
]
}'Deal pipeline automation from email signals
Once email data flows into HubSpot, you can build workflow automations that move deals through your pipeline based on email engagement. HubSpot Workflows (available on Professional and Enterprise plans) trigger on contact or deal property changes.
Properties that update when you log email engagements via the API:
hs_email_last_email_date— timestamp of the most recent emailhs_email_last_reply_date— when the contact last repliednum_associated_deals— count of deals linked to the contacths_sales_email_last_replied— last reply to a sales email specificallynotes_last_updated— timestamp of the most recent note or activity
Example workflows you can build:
- Move deal to “Engaged” stage when a contact on the deal replies to an email for the first time in 14+ days
- Create a follow-up task when an email engagement is logged but no reply comes within 3 days
- Notify the deal owner in Slack when a contact associated with a deal worth $50K+ sends an email
- Update lead score by +10 points each time an email engagement is created (HubSpot's lead scoring uses contact properties)
Python sync with hubspot-api-client
This script handles the full flow: export from Nylas CLI, batch-create contacts (letting HubSpot auto-create companies), log email engagements with associations, and respect rate limits with built-in retry.
#!/usr/bin/env python3
"""Sync Nylas CLI email data to HubSpot CRM via batch API v3."""
import json
import subprocess
import os
import sys
import time
from hubspot import HubSpot
from hubspot.crm.contacts import BatchInputSimplePublicObjectInputForCreate
FREEMAIL = {"gmail.com", "yahoo.com", "outlook.com", "hotmail.com",
"aol.com", "icloud.com", "protonmail.com"}
token = os.environ.get("HUBSPOT_TOKEN")
if not token:
print("Set HUBSPOT_TOKEN env var", file=sys.stderr)
sys.exit(1)
api = HubSpot(access_token=token)
# Export from Nylas CLI
emails = json.loads(subprocess.run(
["nylas", "email", "list", "--json", "--limit", "500"],
capture_output=True, text=True, check=True,
).stdout)
print(f"Exported {len(emails)} emails")
# Deduplicate senders, filter freemail
seen: set[str] = set()
contacts_to_create: list[dict] = []
for msg in emails:
sender = (msg.get("from") or [{}])[0]
addr = sender.get("email", "")
if not addr or addr in seen:
continue
domain = addr.split("@")[1] if "@" in addr else ""
if domain in FREEMAIL:
continue
seen.add(addr)
name = sender.get("name", "")
parts = name.split(" ", 1) if name else ["", ""]
contacts_to_create.append({
"email": addr,
"firstname": parts[0],
"lastname": parts[1] if len(parts) > 1 else "",
})
print(f"{len(contacts_to_create)} unique business contacts to sync")
# Batch create contacts (100 at a time)
# HubSpot auto-creates Company records from email domains
contact_id_map: dict[str, str] = {}
for i in range(0, len(contacts_to_create), 100):
batch = contacts_to_create[i:i+100]
try:
result = api.crm.contacts.batch_api.create(
batch_input_simple_public_object_input_for_create=
BatchInputSimplePublicObjectInputForCreate(
inputs=[{"properties": c} for c in batch]
)
)
for r in result.results:
contact_id_map[r.properties["email"]] = r.id
print(f" Batch {i//100+1}: {len(result.results)} created")
except Exception as e:
# 409 = contact exists — search and get ID
print(f" Batch {i//100+1} partial: {e}")
for c in batch:
try:
search = api.crm.contacts.search_api.do_search({
"filter_groups": [{"filters": [{
"property_name": "email",
"operator": "EQ",
"value": c["email"],
}]}],
"limit": 1,
})
if search.total > 0:
contact_id_map[c["email"]] = search.results[0].id
except Exception:
pass
# Respect rate limits: 100 calls / 10 sec
time.sleep(0.15)
print(f"Mapped {len(contact_id_map)} contacts")
# Log email engagements with Contact associations
engagement_count = 0
for msg in emails[:200]:
sender = (msg.get("from") or [{}])[0]
addr = sender.get("email", "")
contact_id = contact_id_map.get(addr)
if not contact_id:
continue
ts = msg.get("date", "")
if isinstance(ts, int):
from datetime import datetime, timezone
ts = datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()
try:
api.crm.objects.emails.basic_api.create(
simple_public_object_input_for_create={
"properties": {
"hs_timestamp": ts,
"hs_email_direction": "INCOMING_EMAIL",
"hs_email_subject": msg.get("subject", ""),
"hs_email_text": msg.get("snippet", "")[:2000],
"hs_email_status": "SENT",
"hs_email_sender_email": addr,
},
"associations": [{
"to": {"id": contact_id},
"types": [{
"associationCategory": "HUBSPOT_DEFINED",
"associationTypeId": 198,
}],
}],
}
)
engagement_count += 1
except Exception as e:
print(f" Error logging {addr}: {e}", file=sys.stderr)
print(f"Logged {engagement_count} email engagements")
print("Sync complete.")Install and run:
pip install hubspot-api-client
export HUBSPOT_TOKEN="pat-na1-xxxxxxxx" # Private app token
python3 hubspot_sync.pyNext steps
- Export email data to Salesforce — Bulk API 2.0, SOQL queries, and Apex triggers
- Organize emails by company — group your inbox by sender domain before exporting
- Enrich contacts from email — extract job titles and phone numbers from signatures
- Build a contact hierarchy — infer reporting lines from CC patterns
- CRM Email Workflows series — all 8 guides for extracting CRM intelligence
- CLI Command Reference — full list of Nylas CLI commands with flags and examples