Skip to content
Back to Blog
Integration

HubSpot API Integration Guide for SaaS Teams, With Working Requests

Jun 18, 2026
SCSunny Chauhan
HubSpot API Integration Guide for SaaS Teams, With Working Requests

A HubSpot API integration is your product holding one OAuth token per customer account and using it to read and write CRM records at api.hubapi.com. In practice that comes down to five kinds of request. You page through records for the first sync and upsert changes in batches. You link records with associations and find what changed through search or webhooks. And you read each customer's custom properties. Get those five right on the current 2026-09 paths and most of the integration is done. Get one wrong, usually upsert or search, and the problem shows up as duplicate records in a customer's account.

For a SaaS company, the request code is the smaller part of the cost. Every customer's HubSpot account has its own properties, validation rules and limits, and HubSpot releases a new API version every six months. If your engineers are busy with the core product, Appnigma's done-for-you integrations service builds the integration for you, with "ongoing maintenance included." This page is for teams writing the requests themselves. If you're still choosing which HubSpot APIs and versions to build on, start with our HubSpot API builder's guide.

Updated October 2026. Checked against HubSpot's developer documentation, error handling guide and knowledge base. This update moves every request to the date-based 2026-09 paths and corrects the search limit and the claims about record IDs and retries. It adds batch upsert, 207 responses and company deduplication. It also removes a customer scenario that wasn't based on HubSpot sources.

The requests a customer-facing HubSpot integration sends

Almost every SaaS integration with HubSpot is built from the same handful of endpoints. HubSpot's API reference uses date-based versions in the path, so a contacts request now goes to /crm/objects/2026-09/contacts instead of /crm/v3/objects/contacts. The v3 paths still answer, but HubSpot's legacy API changelog ends their support in September 2027.

JobRequest on 2026-09Per requestLimit to plan for
First sync of a customer's recordsGET /crm/objects/2026-09/{object}?limit=100&after=...100 records110 requests per 10 seconds per installing account
Push changes from your productPOST /crm/objects/2026-09/{object}/batch/upsert100 inputsSame 110 per 10 seconds
Link recordsPOST /crm/associations/2026-09/{from}/{to}/batch/create2,000 inputsAssociations have their own burst and daily limits
Find changed recordsPOST /crm/objects/2026-09/{object}/search200 recordsFive requests per second, 10,000 results per query
Read the customer's fieldsGET /crm/properties/2026-09/{object}All definitionsCache the result

The 110 figure comes from HubSpot's usage guidelines, which say "each HubSpot account that installs your app is limited to 110 requests every 10 seconds" for apps distributed through the Marketplace, and that this "excludes the CRM Search API." The budget belongs to the customer's account, so a slow backfill for one customer never touches another customer's limit. It does compete with every other call you make into that same account.

Reading records and paging through a customer's account

A single record comes from GET /crm/objects/2026-09/contacts/{recordId}. Pass properties as a comma-separated list, because HubSpot returns only a default set otherwise. The contacts guide adds two parameters worth knowing. propertiesWithHistory returns past values of a field, and associations returns the IDs of related records in the same call. A property that exists but is empty comes back as null, while a property name that doesn't exist in that account is left out of the response entirely. That second case is how a typo in a field name hides in production.

For the first sync, page through the list endpoint with a cursor. HubSpot returns "up to 100 contacts in one request," and each response carries paging.next.after while more records remain.

curl "https://api.hubapi.com/crm/objects/2026-09/contacts?limit=100&properties=email,firstname,lastname,lifecyclestage" \
  --header "Authorization: Bearer <access_token>"

repeat with &after=<paging.next.after> until paging is missing:

The record ID is a property too. HubSpot's CRM overview says a record's "Record ID (hs_object_id) is automatically generated and should be treated as a string." The id in every response is that same value, not a separate key. Store it as text in your database, since IDs are unique only within one object type and one account. A contact and a company in the same account can share an ID.

Batch read (POST /crm/objects/2026-09/contacts/batch/read) fetches up to 100 known records at once, by ID or by a unique property such as email using idProperty. One catch from the contacts guide is that "the batch endpoint cannot retrieve associations." If you need related records in bulk, read them through the associations batch endpoint as a separate step.

Writing changes without creating duplicates

Use batch upsert for anything your product pushes into HubSpot. The contacts guide says that with /crm/objects/2026-09/contacts/batch/upsert, "if the contacts already exist, they'll be updated and if the contacts don't exist, they'll be created." Each input names the unique property it matches on.

POST /crm/objects/2026-09/contacts/batch/upsert
{
  "inputs": [
    {
      "idProperty": "email",
      "id": "[email protected]",
      "objectWriteTraceId": "evt_8812",
      "properties": { "firstname": "Jane", "lifecyclestage": "customer" }
    }
  ]
}

Upsert matters because HubSpot doesn't retry your API calls for you. Its error handling guide limits retries to cases where "your app or integration provides an endpoint that HubSpot will call," such as webhooks. CRM writes have no idempotency key either. So when your create times out and your worker tries again, a plain POST can make a second contact. An upsert keyed on email, or on your own customer ID stored in a unique property, sends the retry to the same record. HubSpot allows "up to ten unique ID properties per object," according to its properties guide. Creating one for your product's ID at install time is the cleanest key you can get.

Companies need extra care. HubSpot's deduplication article says "companies created through API will not be deduplicated by the Company domain name property. This includes installed third-party sync apps." A sync that creates companies with a plain POST will make a second "Acme" in every account where Acme already exists. Upsert companies on a unique property, or search by domain before you create.

Batch writes can partly succeed. The error handling guide documents a 207 Multi-Status response, "returned when there are different statuses (e.g., errors and successes)," with numErrors and an errors array. Give every input a unique objectWriteTraceId, as in the example above, and each error comes back tagged with it. Without that tag, you can't tell which of the 100 inputs failed. Since September 8, 2026, HubSpot also applies each customer's own validation rules to API writes, according to its CRM write validation changelog. The same payload can pass in one account and fail in another, so record failed inputs per customer account and show them to that customer.

Linking contacts, companies and deals with associations

An association is a link between two records, and on 2026-09 it carries a type. The simplest call creates the default, unlabeled link: PUT /crm/objects/2026-09/contacts/{contactId}/associations/default/companies/{companyId}. For a specific type, send the type in the body of PUT /crm/objects/2026-09/contacts/{contactId}/associations/deals/{dealId}.
[ { "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 4 } ]

HubSpot's associations guide lists the common type IDs. Contact to company is 279, contact to primary company is 1, contact to deal is 4 and contact to ticket is 15. Custom labels a customer creates use USER_DEFINED and get IDs only that account knows, so read them from /crm/associations/2026-09/{from}/{to}/labels at install instead of hardcoding them. The old v3 style, with names like contact_to_deal in the path, is one of the APIs that loses support in September 2027.

Associations have a larger batch size and a separate budget. Batch create takes "2,000 inputs per request body." Burst limits are 150 requests per 10 seconds on Professional and Enterprise accounts, with 500,000 a day. Linking at create time saves calls too, because the create body accepts an associations array. A new contact can be created already attached to its company.

Search is how most integrations ask "what changed since my last sync." Filter on lastmodifieddate greater than your last checkpoint, sort ascending on the same property, and page through the results. HubSpot's search guide sets the limits. Search allows "five requests per second per account," returns up to 200 records per page, and accepts at most five filter groups with 18 filters in total.

POST /crm/objects/2026-09/contacts/search
{
  "filterGroups": [ { "filters": [
    { "propertyName": "lastmodifieddate", "operator": "GT", "value": "1761177600000" }
  ] } ],
  "sorts": [ { "propertyName": "lastmodifieddate", "direction": "ASCENDING" } ],
  "properties": ["email", "lifecyclestage"],
  "limit": 200
}

Three behaviors from that guide break naive sync code. First, the guide says search is "limited to 10,000 total results for any given query" and that paging past that point "will result in a 400 error" for the request. When a large account changes more than 10,000 records at once, start a new query from the last lastmodifieddate you saw instead of paging further. Second, "it may take a few moments for newly created or updated CRM objects to appear in search results," so overlap each checkpoint by a minute and let upserts absorb the repeats. Third, search responses "will not include any of the rate limit headers," which means your client has to pace itself at five per second without feedback from HubSpot.

Search is the wrong tool for a backfill. A customer with 50,000 contacts is five times over the 10,000-result cap. Use the paged list endpoint for the first sync, then search or HubSpot webhooks for changes. Webhooks deliver changes without polling, and HubSpot retries failed deliveries "up to 10 times" over 24 hours.

What one customer's daily sync costs in requests

A Marketplace app gets 110 requests every 10 seconds in each installing account, an object batch carries 100 records, and an association batch carries 2,000. The table works through what that means for one customer whose account receives 3,000 changed contacts a day from your product, each linked to a company. It is planning arithmetic from HubSpot's documented limits, not a benchmark.

ApproachRequests per dayTime at 110 per 10 seconds
Single create or update per contact3,000About 4.5 minutes of that account's budget
Batch upsert, 100 per request30Under 3 seconds
Company links, one call each3,000 moreAnother 4.5 minutes
Company links, batch create at 2,0002Negligible
Search for changes every 5 minutes288 or morePaced separately at 5 per second

The batch route costs about 320 requests a day instead of about 6,300, and almost all of what remains is polling. Swapping the five-minute poll for webhooks removes most of that too. For a Marketplace app, the gap matters beyond speed. HubSpot's usage guidelines say error responses "shouldn't exceed 5% of your total daily requests" for an app to be certified, and fewer, larger requests make each failure easier to find and fix.

Reading each customer's custom properties

Every customer account has its own fields, and your field mapping screen depends on them. GET /crm/properties/2026-09/contacts returns every property definition for that account, with its internal name, type and options. Internal names are fixed once created. HubSpot's property settings article says "you cannot edit the property's object type or internal name," so mapping by internal name is safe. Labels and dropdown options can change, which is why you read them per account instead of shipping a fixed list.

Cache the definitions per account. HubSpot's usage guidelines say settings such as "your object properties, owners" should be cached when you call for them repeatedly. Refresh the cache when a customer opens your mapping screen, and when a write fails because a property or option no longer exists.

Which HubSpot API errors should your integration retry?

The error body has a status, message, category and correlationId. HubSpot's error handling guide says these fields "should all be treated as optional in any error parsing" by your client. Keep the correlationId in your logs, since it's what HubSpot support asks for. The status code decides what your worker does next.

StatusWhat it meansWhat the integration should do
207Batch partly succeededRetry nothing blindly. Read errors, match on objectWriteTraceId, and surface the failed records
400Invalid request or a customer's validation rule rejected itDon't retry. Show the message to that customer's admin
401Invalid authenticationRefresh on schedule from expires_in, not after a 401
403Token lacks the scopeAsk the customer to reinstall with the scope, or hide the feature
423Record locked during heavy writesWait at least 2 seconds, then retry
429Rate limit hitBack off, then retry
502, 503, 504, 524Temporary HubSpot-side problemPause a few seconds, then retry

Two rows need a note. HubSpot's usage guidelines say "Unauthorized (401) requests are not a valid indicator that a new access token must be retrieved." Access tokens expire after 30 minutes, so refresh them per account before they lapse. The 423 lock is documented as a result of things like "upserting thousands of company records in a very short period," which a first sync for a large customer can do.

Mistakes that show up after customers install

Each of these traces back to HubSpot's documentation, and each one appears only once real customer accounts are connected.

  • Creating companies with a plain POST. API-created companies aren't deduplicated by domain, so every sync can add copies.
  • Treating hs_object_id as a number or as globally unique. It's a string, unique within one object type in one account.
  • Retrying a timed-out create. HubSpot has no idempotency key, so use upsert on a unique property.
  • Paging search past 10,000 results. It returns a 400 halfway through a large account.
  • Hardcoding association type IDs for custom labels. They differ in every account.
  • Writing new code against /crm/v3/ paths. Support ends in September 2027.

Who builds and maintains a HubSpot integration

A HubSpot integration keeps needing work after launch. HubSpot ships a new API version every six months, customer admins add validation rules, and each new customer brings different fields to map. Some SaaS companies would rather keep their engineers on the core product. Appnigma's done-for-you integrations service builds customer-facing integrations with HubSpot and Salesforce and includes ongoing maintenance. Teams that want their own engineers to build can use Appnigma's integration infrastructure, which provides deployment and monitoring per customer connection. If the goal is a listing on the HubSpot Marketplace, see Appnigma's marketplace apps service and our guide to building a HubSpot app.

Frequently Asked Questions

How do I avoid duplicate records when syncing to HubSpot?

Use batch upsert with idProperty set to email for contacts, or to a unique property holding your own ID. HubSpot doesn't deduplicate companies created through the API by domain, so never create companies with a plain POST.

Does HubSpot retry failed API requests?

No. HubSpot retries only calls it makes to your endpoints, such as webhooks, so your code owns retries for every API request it sends.

You can't page past 10,000 in one query, because HubSpot returns a 400. Sort ascending on lastmodifieddate or hs_object_id and start a new query from the last value you saw, or use the list endpoint for full reads.

What is the HubSpot search API rate limit?

Five requests per second per account, and search responses carry no rate limit headers. It is separate from the 110 requests per 10 seconds that Marketplace apps get per installing account.

Can Appnigma build our HubSpot integration?

Yes. Appnigma's done-for-you service builds and maintains customer integrations with HubSpot and Salesforce, and its marketplace apps service builds native HubSpot apps with listing and review support.

About the author

Sunny Chauhan is the co-founder and CEO of Appnigma AI, which builds and maintains customer integrations and native Salesforce and HubSpot apps for B2B SaaS teams. Before Appnigma, he was on Salesforce's AppExchange team, where he worked with more than 300 SaaS partners. He has spent close to a decade on the Salesforce platform, including work at Zennify, and holds Salesforce certifications including Platform Developer II, Platform App Builder, Administrator, Data Cloud Consultant, and AI Associate.

Sources

  1. API reference overview and Understanding the CRM, HubSpot Developers
  2. Contacts API guide, HubSpot Developers
  3. Associations guide, HubSpot Developers
  4. Search the CRM, HubSpot Developers
  5. Properties guide, HubSpot Developers, and Create and edit properties, HubSpot Knowledge Base
  6. Error handling and API usage guidelines and limits, HubSpot Developers
  7. Deduplication of records, HubSpot Knowledge Base
  8. Legacy APIs and legacy apps, what's going unsupported and when and CRM API write validation, HubSpot Developer Changelog

All Blogs

Bring the customer requirements and your current setup. Get a clear scope and delivery timeline to take back to them.