csvjson
HubSpot CRM API

Convert HubSpot API JSON to CSV

HubSpot's CRM API wraps every contact, company, and deal inside a properties object. Flattening that one level is all it takes to get a clean CSV — but pagination and custom property names require a bit of extra handling.

HubSpot exports are a common request from sales and marketing teams: contact lists for email campaigns, deal pipelines for reporting, company data for segmentation. The API wraps all field values inside a nested 'properties' key, with pagination handled via a 'paging.next.after' cursor. This guide shows how to flatten the properties, handle all page results, and export to CSV in a format ready for Excel or your CRM of choice.

Want to skip the code? Paste your JSON directly into the converter — it handles nested objects, arrays, and large files automatically.

Open JSON to CSV Converter

What the API returns

HubSpot GET /crm/v3/objects/contacts — single page of results

{
  "results": [
    {
      "id": "1234567",
      "properties": {
        "email": "alice@example.com",
        "firstname": "Alice",
        "lastname": "Chen",
        "company": "Acme Corp",
        "jobtitle": "VP Engineering",
        "phone": "512-555-0100",
        "hs_lead_status": "IN_PROGRESS",
        "lifecyclestage": "customer",
        "createdate": "2025-01-15T09:30:00.000Z",
        "lastmodifieddate": "2025-03-20T14:22:00.000Z",
        "hs_object_id": "1234567"
      },
      "createdAt": "2025-01-15T09:30:00.000Z",
      "updatedAt": "2025-03-20T14:22:00.000Z",
      "archived": false
    }
  ],
  "paging": {
    "next": {
      "after": "9876543",
      "link": "https://api.hubapi.com/crm/v3/objects/contacts?after=9876543"
    }
  }
}

Field mapping: JSON path → CSV column

JSON pathCSV columnNotes
idrecord_idHubSpot internal ID
properties.emailemail
properties.firstnamefirst_name
properties.lastnamelast_name
properties.companycompany
properties.jobtitlejob_title
properties.phonephone
properties.hs_lead_statuslead_statusHubSpot default property
properties.lifecyclestagelifecycle_stagelead, customer, evangelist, etc.
properties.createdatecreated_atISO 8601 timestamp
properties.lastmodifieddatelast_modified
archivedarchivedtrue = deleted/archived contact

Custom properties follow the same pattern as built-in ones — they live inside the properties object. Request them explicitly via the ?properties= query parameter.

Python conversion

Fetch all contacts and export to CSV (handles pagination)

import requests
import pandas as pd

API_KEY = "pat-na1-..."  # HubSpot private app token
BASE_URL = "https://api.hubapi.com/crm/v3/objects/contacts"

# Specify which properties to fetch
PROPERTIES = [
    "email", "firstname", "lastname", "company",
    "jobtitle", "phone", "hs_lead_status", "lifecyclestage",
    "createdate", "lastmodifieddate",
]

headers = {"Authorization": f"Bearer {API_KEY}"}
params = {"limit": 100, "properties": ",".join(PROPERTIES)}

all_contacts = []
url = BASE_URL

while url:
    r = requests.get(url, headers=headers, params=params)
    r.raise_for_status()
    data = r.json()
    all_contacts.extend(data["results"])

    # Follow pagination cursor
    after = data.get("paging", {}).get("next", {}).get("after")
    url = BASE_URL if after else None
    if after:
        params = {"limit": 100, "properties": ",".join(PROPERTIES), "after": after}

print(f"Fetched {len(all_contacts)} contacts")

# Flatten: pull properties up to the top level
rows = []
for contact in all_contacts:
    row = {"record_id": contact["id"], "archived": contact.get("archived", False)}
    row.update(contact.get("properties", {}))
    rows.append(row)

df = pd.DataFrame(rows)
df.to_csv("hubspot_contacts.csv", index=False, encoding="utf-8-sig")
print(f"Exported {len(df)} rows to hubspot_contacts.csv")

Export deals with associated company name

import requests
import pandas as pd

API_KEY = "pat-na1-..."
headers = {"Authorization": f"Bearer {API_KEY}"}

DEAL_PROPS = ["dealname", "amount", "dealstage", "closedate", "pipeline", "hubspot_owner_id"]

url = "https://api.hubapi.com/crm/v3/objects/deals"
params = {
    "limit": 100,
    "properties": ",".join(DEAL_PROPS),
    "associations": "companies",  # include associated company IDs
}

all_deals = []
while url:
    r = requests.get(url, headers=headers, params=params)
    data = r.json()
    all_deals.extend(data["results"])
    after = data.get("paging", {}).get("next", {}).get("after")
    url = "https://api.hubapi.com/crm/v3/objects/deals" if after else None
    if after:
        params = {**params, "after": after}

rows = []
for deal in all_deals:
    props = deal.get("properties", {})
    # Extract first associated company ID
    companies = deal.get("associations", {}).get("companies", {}).get("results", [])
    company_id = companies[0]["id"] if companies else None
    rows.append({
        "deal_id":    deal["id"],
        "deal_name":  props.get("dealname"),
        "amount":     props.get("amount"),
        "stage":      props.get("dealstage"),
        "close_date": props.get("closedate", "")[:10] if props.get("closedate") else "",
        "pipeline":   props.get("pipeline"),
        "company_id": company_id,
    })

pd.DataFrame(rows).to_csv("hubspot_deals.csv", index=False, encoding="utf-8-sig")

Common issues with this API

All property values are strings — even numbers and dates

HubSpot returns every property value as a string, including numeric fields like 'amount' and date fields like 'closedate'. Cast them explicitly before analysis.

df["amount"] = pd.to_numeric(df["amount"], errors="coerce")
df["closedate"] = pd.to_datetime(df["closedate"], errors="coerce").dt.strftime("%Y-%m-%d")

You must request properties explicitly

The CRM API only returns id, createdAt, updatedAt, and archived by default. To get contact fields, add ?properties=email,firstname,lastname (or properties[]=email&properties[]=firstname) to every request. Omitting this is the most common reason for an empty-looking export.

# Wrong — only gets id/createdAt/updatedAt/archived
requests.get(url, headers=headers)

# Correct — explicitly request fields
requests.get(url, headers=headers, params={"properties": "email,firstname,lastname,company"})

hs_object_id vs id

Each record has both an 'id' field at the top level and a 'properties.hs_object_id' inside the properties object. They're the same value. Use the top-level 'id' — don't include hs_object_id as a separate column.

Frequently asked questions

How do I export custom HubSpot properties?

Add the internal property name to your properties parameter. Find internal names in HubSpot under Settings → Properties → hover over a property → 'Copy internal name'. They look like 'my_custom_field_name'. Request them the same way as built-in properties.

Can I export contacts, companies, and deals together in one CSV?

Not directly — they're separate objects with separate API endpoints. Export each separately, then join in pandas or Excel using the association IDs. HubSpot's associations API gives you the contact-to-company and deal-to-contact mappings.

Why does my export have fewer contacts than HubSpot shows?

The CRM API excludes archived records by default. Add archived=true to your request to include deleted contacts: GET /crm/v3/objects/contacts?archived=true. Alternatively, HubSpot may have deduplication limits or portal-specific API rate limits that interrupted pagination.

Does the online converter work with HubSpot JSON?

Yes. Export from HubSpot or save an API response to a .json file, then paste it into the JSON to CSV converter. The converter flattens the nested properties object automatically.

Related tools