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 ConverterWhat 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 path | CSV column | Notes |
|---|---|---|
| id | record_id | HubSpot internal ID |
| properties.email | ||
| properties.firstname | first_name | |
| properties.lastname | last_name | |
| properties.company | company | |
| properties.jobtitle | job_title | |
| properties.phone | phone | |
| properties.hs_lead_status | lead_status | HubSpot default property |
| properties.lifecyclestage | lifecycle_stage | lead, customer, evangelist, etc. |
| properties.createdate | created_at | ISO 8601 timestamp |
| properties.lastmodifieddate | last_modified | |
| archived | archived | true = 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.