Source: https://cli.nylas.com/guides/export-email-to-hubspot

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 any email provider using Nylas CLI and pushes it into HubSpot via batch API endpoints, the association API, and timeline events.

Written by [Prem Keshari](https://cli.nylas.com/authors/prem-keshari) • Senior SRE

Reviewed by [Nick Barraclough](https://cli.nylas.com/authors/nick-barraclough)

Updated April 11, 2026

Verified

 —

CLI

3.1.1

 ·

Gmail, Outlook

 ·

last tested

April 11, 2026

> **TL;DR:** HubSpot auto-creates Company records from email domains and deduplicates Contacts by address. Export with `nylas email list --json` and `nylas contacts list --json`, filter freemail domains, then batch-create via API v3. This guide covers association types, timeline events, and deal automation triggers.

> **Part of the CRM Email Workflows series.** This guide is one of 8 covering how to extract CRM intelligence from your inbox. Start here if HubSpot is your team's CRM, especially on the free or Starter tier. [View the full series →](https://cli.nylas.com/guides/crm-email-workflows)

## 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 and filter for HubSpot

HubSpot auto-creates Company records from email domains. That's useful for business contacts but creates junk records from freemail addresses. Export your data, then strip freemail senders before the import.

File: `hubspot-export.sh`

```bash
# Pull email history and contacts into local JSON
nylas email list --json --limit 500 > emails.json
nylas contacts list --json --limit 500 > contacts.json

# HubSpot-specific: strip freemail domains to prevent junk Company records
# HubSpot auto-creates Companies from email domains, so gmail.com creates
# a Company called "gmail.com" -- filter these out before import
cat emails.json | jq '[.[] | select(
  .from[0].email | split("@")[1] |
  IN("gmail.com","yahoo.com","outlook.com","hotmail.com") | not
)]' > emails-business.json
```

## Automatic 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 contacts
- `POST /crm/v3/objects/contacts/batch/update` — update up to 100 contacts by ID
- `POST /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

File: `batch-create.sh`

```bash
# 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:

File: `timeline-event.sh`

```bash
# 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 |
| Email | Contact | 198 | Email to contact |
| Company | Deal | 5 | Company to deal |

File: `associations.sh`

```bash
# 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 email
- `hs_email_last_reply_date` — when the contact last replied
- `num_associated_deals` — count of deals linked to the contact
- `hs_sales_email_last_replied` — last reply to a sales email specifically
- `notes_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.

File: `hubspot_sync.py`

```python
#!/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:

File: `run.sh`

```bash
pip install hubspot-api-client

export HUBSPOT_TOKEN="pat-na1-xxxxxxxx"  # Private app token

python3 hubspot_sync.py
```

## Next steps

- [Export email data to Salesforce](https://cli.nylas.com/guides/export-email-to-salesforce) — Bulk API 2.0, SOQL queries, and Apex triggers
- [Organize emails by company](https://cli.nylas.com/guides/organize-emails-by-company) — group your inbox by sender domain before exporting
- [Enrich contacts from email](https://cli.nylas.com/guides/enrich-contacts-from-email) — extract job titles and phone numbers from signatures
- [Build a contact hierarchy](https://cli.nylas.com/guides/contact-hierarchy-from-email) — infer reporting lines from CC patterns
- [CRM Email Workflows series](https://cli.nylas.com/guides/crm-email-workflows) — all 8 guides for extracting CRM intelligence
- [CLI Command Reference](https://cli.nylas.com/docs/commands) — full list of Nylas CLI commands with flags and examples
