HubSpot API: The Complete Developer Guide (and for Anyone Tired of Broken Integrations)
Let me guess how you got here. Someone asked you to "pull the contacts from HubSpot into our system", you figured it would be an afternoon of work, and three days later you are still hunting down why the integration brings 9,900 records and stops. Or worse: it used to run, and on some random night it started returning a 429 nobody understands.
If you recognized yourself, this guide is for you. The HubSpot API is powerful, but it has its own rules that are not obvious on a first read of the docs. Whoever learns these rules writes integrations that run for months with nobody touching them. Whoever does not keeps rebuilding the same script every time the base grows.
The good news: the rules are few and make sense once you see them together. I will show you all of them, in the order they matter, with examples you can copy. By the end, you will know how to read, write, associate, and automate in HubSpot without falling into the classic traps.
What the HubSpot API really solves
The HubSpot API lets you do programmatically everything you would do in the interface, and at scale: read and write CRM data, capture form leads, build automations, and connect HubSpot to ERPs, BI tools, and other systems. The portal becomes a piece of your ecosystem, not an island.
In practice, three uses account for most projects. The first is to mirror data: take contacts, companies, and deals into a data warehouse or database, which a BI tool like Power BI then reads from. The second is to sync systems: keep HubSpot and an ERP speaking the same language. The third is to react to events: trigger a process when a deal changes stage. Everything else is a variation of those three.
The base everyone gets wrong first: authentication
Every call to the HubSpot API goes out from the same base, https://api.hubapi.com, and the affected portal is set by the token, not by the URL. That has a direct consequence: the token is the most sensitive item in the whole operation. Whoever holds the token holds the portal.
1. Private app or OAuth?
There are two authentication paths, and choosing the wrong one costs rework. The private app serves a single portal, gets a fixed token, and is the simplest path for internal integrations. OAuth serves apps that many portals will install, with a consent flow and tokens that renew. The rule is simple: one portal, private app; many portals, OAuth.
2. Scopes: grant only what the task needs
Each token receives scopes, which are permissions. Reading contacts requires crm.objects.contacts.read, writing requires crm.objects.contacts.write, and so on. The principle is least privilege: a read token should not have write scopes. This reduces the damage if the token leaks and makes it easier to audit who did what.
|
Authorization: Bearer <private_app_token> Content-Type: application/json |
|
A tip from someone who has been burned: if you get a 403 even with the scope enabled, on a static credential the usual causes are that the scope was not saved, is the wrong one for the endpoint, or you are confusing read with write. Adding the scope and clicking Save applies it to the existing token, with no regeneration. On OAuth, reinstall and re-consent. |
3. Treat the token as a secret, no exceptions
The token must never appear in a collection body, in a URL, in a log, or in a code repository. Always store it in a credential vault: the Postman Vault, the n8n or Make credential manager, or protected environment variables. Use one token per integration, so you can revoke one without taking down the others, and rotate tokens periodically.
Reading data without missing records
Reads in HubSpot come in pages, and pagination is by cursor. The response carries a cursor in paging.next.after, and you repeat the call passing that value until the cursor disappears. The golden rule, which solves half the extraction bugs: on the first call, do not send after. Some endpoints reject an empty after.
|
GET /crm/v3/objects/contacts?limit=100&properties=email,firstname // the response carries paging.next.after = "cursor123" GET /crm/v3/objects/contacts?limit=100&properties=email,firstname&after=cursor123 |
Listing, search, and batch are not the same
There are three ways to read, and using the wrong one is what makes the integration stop at 9,900 records. The table below sums up when to use each.
|
Way |
When to use |
The limit that catches you |
|---|---|---|
|
Listing (GET .../objects/{type}) |
Complete extraction of an object |
100 per page, cursor pagination |
|
Search (POST .../search) |
A slice by date, stage, or value |
10,000 cap per filter set |
|
Batch read (POST .../batch/read) |
You already have the ids |
100 per batch, no cursor |
That classic case of stopping at 10,000 is the Search API hitting its cap. The solution is not to insist, it is to break the search into ranges, for example by creation date windows, and paginate each range separately. Each range stays below the cap and the set covers the whole base.
Writing to the CRM without creating duplicates
Writing in HubSpot is done in batch, up to 100 records per request. And here lies the costliest trap: batch creation is not idempotent. Running the same load twice creates duplicates, and duplicate data in HubSpot erodes trust in reports and costs money. So write collections run once, with the status checked and the map of returned ids saved for reconciliation.
|
POST /crm/v3/objects/contacts/batch/create { "inputs": [ { "properties": { "email": "ana@example.com", "firstname": "Ana" } }, { "properties": { "email": "leo@example.com", "firstname": "Leo" } } ] } |
Batch update uses the record id, and ids within the same batch must be unique. If the same person appears twice in the input list, the API rejects the whole batch with a duplicate id error. Deduplicate by id before assembling the batches. And watch the 207 status, which means partial success: part of the batch passed, part failed. Always read the response body to separate who got in from who errored out.
Associations: connecting records to each other
A CRM is not a loose list of contacts, it is a web. Associations connect records of different objects: a contact to a company, an activity to a deal. Beyond the standard types, you create custom labels, like "Decision Maker" or "Billing", to name the relationship. When in doubt about which type id to use, query the labels endpoint before associating at volume, because the ids vary by object pair.
Automation: reacting to events with webhooks and workflows
Automating via API goes beyond visual workflows. You create flows via Automation v4, usually disabled, to review and turn on in the interface later. But there is an important limit: via API you can only create filter-based enrollment criteria, like property value and list membership. Event triggers, such as form submission, must be configured in the interface. To react to changes in real time, use webhooks, handling idempotency so you do not process the same event twice.
Where the API disappoints you (and that is fine)
Part of mastering the HubSpot API is knowing what it does not do, so you do not waste a day trying. The table below gathers the limits that surprise people most.
|
Area |
The limitation |
How to handle |
|---|---|---|
|
Forms |
New forms are not creatable via API, only legacy |
Build the new form in the interface |
|
Workflows |
Event triggers are not creatable via API |
Create the flow via API, set the trigger in the interface |
|
Calculated fields |
Rollups are read-only |
Do not try to write; derive via workflow |
|
Properties |
You cannot change the type of an existing property |
Create a new property and migrate the values |
Rate limits: the 429 will happen
Every integration hits the 429 error sooner or later. It is not a defect in your code, it is the platform's rate control warning you that you went over the limit for the period. What separates a fragile integration from a robust one is the reaction: respecting the header that says when to retry and retrying with exponential backoff. The Search API has its own, lower limit, so the 429 tends to show up there first.
You can do all of this without coding
A detail few people notice: the same endpoints can be operated by non-technical people in visual tools. The logic is always the same, one authentication with the token, one call to an endpoint, and handling the response page by page. Postman organizes calls into collections and runs them in sequence. n8n builds visual flows with a native HubSpot block and an HTTP block. Make works through scenarios with ready-made HubSpot modules and an iterator for each page.
In practice: when the integration is well built
Two scenarios we see often. In one, an operation extracted deals into a dashboard and swore data was missing, when the search was hitting the 10,000 cap; breaking it into date windows fixed it. In another, a nightly contact load made the base swell endlessly, because batch creation is not idempotent and the routine did not check whether the contact already existed; searching by email before creating and deduplicating by id stopped the phantom growth. The single source of truth starts with not duplicating on the way in.
|
A reflection for operation leaders: an integration is not a project with an end date, it is an asset that needs governance. Token in the vault, minimal scopes, reversible writes, and 429 monitoring. These four habits separate the integration that sleeps soundly from the one that wakes the team at three in the morning. |
Checklist: is your HubSpot integration mature?
- Is the token in a credential vault, with minimal scopes and one token per integration?
- Does reading use cursor pagination, omitting after on the first call?
- Are large searches broken into ranges so they do not hit the 10,000 cap?
- Is the sync incremental, by modification date, with an overlap window and id deduplication?
- Does writing run once, read the 207 status, and store the id map for reconciliation?
- Is there 429 handling with exponential backoff and a concurrency limit?
Frequently asked questions
Do I need to know how to code to use the HubSpot API?
Not necessarily. The same endpoints can be operated in visual tools like Postman, n8n, and Make. The logic is always the same: authenticate with the token, call the endpoint, and handle the response page by page.
What is the difference between a private app and OAuth?
A private app serves a single portal and uses a fixed token, ideal for internal integrations. OAuth serves apps that many portals will install, with consent and renewable tokens. Reach decides which to use.
Why does my extraction stop at 10,000 records?
It is the Search API cap per filter set. Do not insist on the same search: break it into ranges, for example by creation date windows, and paginate each range separately to cover the whole base.
Why do I get a 403 even with the scope enabled?
On a static credential (private app or service key), a saved scope applies to the existing token immediately, so check that it was saved on the right app, that it is the correct scope for the endpoint, and that you are not confusing read with write. Regenerating does not add a scope. On OAuth, reinstall and re-consent.
Is creating records in batch safe to run twice?
No. Batch creation is not idempotent: running it twice creates duplicates. Run write collections once, check the status, and if you need to repeat, search for the record before creating.
What do I do when the API returns 429?
Wait and retry with exponential backoff, respecting the header that indicates when to retry. To prevent it, use batch, request only the necessary properties, and limit concurrency.
Want to get a HubSpot integration off the ground without falling into these traps, or fix one that keeps breaking? At Insight Sales, as a HubSpot partner, we build integrations with the token in the vault, reversible writes, and real monitoring. Talk to our team and find out how we can help.
Ready to take your operation to the next level?
Talk to a specialist and see how we can help.