
Updated June 18, 2026 by Sunny Chauhan.
The shortest answer to "how do I integrate with HubSpot's API" is "depends on what you're trying to do." HubSpot has a CRM API for CRUD against contacts/deals/tickets/companies, an Events API for analytics, a CMS API for managing content, a Conversations API for help-desk integrations, and a half-dozen more. For most SaaS integrations, the CRM API is what you actually want. Here's the complete walkthrough: which API to pick, how to authenticate, the data model, pagination, batching, and the patterns that survive production load.
Pro Tip
TL;DR For most SaaS-to-HubSpot integrations, the CRM API v3 is the right choice. Authenticate with OAuth (Marketplace apps) or Private App tokens (internal integrations). Use batch endpoints whenever possible (100 records per call vs 1). Paginate with
aftercursor, notoffset(HubSpot uses cursor pagination on v3). Webhook-driven for change detection, not polling. Respect the 100 requests / 10 seconds per portal limit. Map your data model to HubSpot's CRM object schema before writing any code.
Picking the right HubSpot API
HubSpot's API surface in 2026:
→ CRM API v3. Contacts, Companies, Deals, Tickets, Line Items, Quotes, Products, Custom Objects. The workhorse for most integrations.
→ Events API. Tracking custom events on contacts. For analytics and product-usage syncing.
→ CMS API. Pages, blog posts, files. For content marketing tooling.
→ Conversations API. Inbox messages, conversations. For help-desk integrations.
→ Marketing API. Forms, emails, marketing events. For marketing automation tooling.
→ Workflows API. Triggering and reading workflows. For automation integrations.
→ Files API. Uploaded files.
Three quick decision rules:
1/ Syncing CRM data (contacts, deals, etc.)? CRM API v3. 2/ Tracking product usage and analytics? Events API. 3/ Integrating with HubSpot's helpdesk inbox? Conversations API.
For most integrations I see in 2026, the CRM API does 80% of the work, and a second API (often Events or Conversations) does the remaining 20%.
Authentication, recap
Three mechanisms, picked by use case:
→ OAuth. Required for Marketplace apps. Multi-tenant. Tokens are short-lived and refresh. → Private App tokens. For internal integrations (one HubSpot portal, one tool). Long-lived. → Legacy API key. Deprecated. Don't use for new builds.
See our HubSpot API key management guide for the full breakdown.
For the rest of this guide I'll assume OAuth (the Marketplace pattern). For Private App integrations the API mechanics are identical; just the token source differs.
The CRM API data model
HubSpot's CRM is built around objects, properties, and associations.
→ Objects. Contacts, Companies, Deals, Tickets, etc. Each has a type id (e.g., 0-1 for Contact, 0-2 for Company, 0-3 for Deal).
→ Properties. Fields on each object. HubSpot has standard properties (firstname, email, lifecyclestage) and customer-defined custom properties.
→ Associations. Links between objects. A contact can be associated with a company, multiple deals, multiple tickets, etc. Associations have labels and types.
→ Custom objects. Customers can define their own object types beyond the standard ones. Your integration may or may not need to support these.
Every object has a unique id (HubSpot-assigned, numeric) and an optional hs_object_id (alias). Most APIs accept either.
Reads: get an object, paginate a list
Single object:
``javascript const response = await fetch( https://api.hubapi.com/crm/v3/objects/contacts/${contactId}, { headers: { 'Authorization': Bearer ${accessToken} }} ); const contact = await response.json(); ``
By default the response includes basic properties only. To get specific properties:
``javascript const url = https://api.hubapi.com/crm/v3/objects/contacts/${contactId} + ?properties=firstname,lastname,email,lifecyclestage; ``
To paginate a list of contacts:
```javascript let after; const allContacts = [];
do { const url = new URL('https://api.hubapi.com/crm/v3/objects/contacts'); url.searchParams.set('limit', '100'); if (after) url.searchParams.set('after', after); url.searchParams.set('properties', 'firstname,lastname,email');
const response = await fetch(url.toString(), { headers: { 'Authorization': Bearer ${accessToken} } }); const data = await response.json();
allContacts.push(...data.results); after = data.paging?.next?.after; } while (after); ```
Worth flagging: HubSpot uses cursor pagination on v3 (after token), not offset. If your code assumes offset-based pagination, it won't work.
Batch reads: 100 records in one call
The batch read endpoint reads up to 100 records by id in a single API call:
``javascript const response = await fetch( 'https://api.hubapi.com/crm/v3/objects/contacts/batch/read', { method: 'POST', headers: { 'Authorization': Bearer ${accessToken}, 'Content-Type': 'application/json' }, body: JSON.stringify({ properties: ['firstname', 'lastname', 'email'], inputs: contactIds.map(id => ({ id })) }) } ); const { results } = await response.json(); ``
100 records = 1 API call (1 rate-limit token). This is the single biggest optimization for high-volume integrations.
Writes: create, update, batch
Single create:
``javascript const response = await fetch( 'https://api.hubapi.com/crm/v3/objects/contacts', { method: 'POST', headers: { 'Authorization': Bearer ${accessToken}, 'Content-Type': 'application/json' }, body: JSON.stringify({ properties: { firstname: 'Jane', lastname: 'Doe', email: 'jane@example.com' } }) } ); ``
Single update (PATCH /crm/v3/objects/contacts/{contactId}) follows the same pattern.
Batch create / update: same endpoints with /batch/create or /batch/update, body is { inputs: [...] }. Up to 100 inputs per call.
Associations: linking records
To associate a contact with a deal:
``javascript await fetch( https://api.hubapi.com/crm/v3/objects/contacts/${contactId}/associations/deals/${dealId}/contact_to_deal, { method: 'PUT', headers: { 'Authorization': Bearer ${accessToken} } } ); ``
The association type (contact_to_deal) is one of HubSpot's predefined association types. Custom associations exist too but are an advanced topic.
To find associations on an object:
``javascript const response = await fetch( https://api.hubapi.com/crm/v3/objects/contacts/${contactId}/associations/deals, { headers: { 'Authorization': Bearer ${accessToken} }} ); const { results } = await response.json(); ``
Search: when filtering matters
The search endpoint accepts filter queries:
``javascript const response = await fetch( 'https://api.hubapi.com/crm/v3/objects/contacts/search', { method: 'POST', headers: { 'Authorization': Bearer ${accessToken}, 'Content-Type': 'application/json' }, body: JSON.stringify({ filterGroups: [{ filters: [{ propertyName: 'lifecyclestage', operator: 'EQ', value: 'customer' }] }], properties: ['firstname', 'lastname', 'email'], limit: 100 }) } ); ``
The search API has a stricter rate limit: 4 requests / second per portal. If you're searching frequently, cache results.
Custom properties: discover them dynamically
Customers define their own properties. Your integration should discover them at runtime rather than hard-coding standard ones.
``javascript const response = await fetch( 'https://api.hubapi.com/crm/v3/properties/contacts', { headers: { 'Authorization': Bearer ${accessToken} }} ); const { results } = await response.json(); ``
Cache this. Property definitions change rarely.
Patterns that survive production load
Four patterns that come up over and over:
→ Cache property schema reads. They almost never change. 12-hour TTL minimum.
→ Batch all multi-record operations. Single-record calls in a loop are an anti-pattern. The batch endpoints exist for a reason.
→ Use webhooks for change detection. See our HubSpot webhooks guide. Polling consumes rate limit; webhooks don't.
→ Implement idempotency on writes. HubSpot's API will sometimes retry on your behalf. Your write code should handle the same payload twice without creating duplicates.
Error handling: the 5 statuses you'll see
→ 200/201: Success. → 400: Bad request. Your payload is malformed. → 401: Unauthorized. Token is invalid or expired. Refresh and retry. → 404: Object doesn't exist. Common when syncing across systems with deleted records. → 429: Rate limited. Back off and retry. See our rate limits guide. → 500/502/503: HubSpot-side error. Retry with backoff.
Your client code should distinguish these. A 404 doesn't deserve the same retry policy as a 429.
How no-code generation changes the calculus
At Appnigma we generate Salesforce 2GP Managed Packages where Apex code handles CRM CRUD without ISV engineering effort. The HubSpot equivalent (no-code generation of a full CRM integration) is less mature in 2026 but emerging. Until the no-code tools catch up, engineering an HubSpot custom integration means writing the patterns above by hand or using a connector platform.
Pre-flight checklist before deploying a HubSpot API integration
[ ] Picked the right API for the job (CRM, Events, Conversations, etc.) → Yes / No
[ ] OAuth implemented with token storage encrypted at rest → Yes / No
[ ] Per-portal rate limiter implemented (token bucket, 100/10s) → Yes / No
[ ] Batch endpoints used for any multi-record operation → Yes / No
[ ] Cursor pagination (
after) implemented correctly → Yes / No[ ] Property schema cached → Yes / No
[ ] Webhooks subscribed for relevant object changes → Yes / No
[ ] Idempotent write logic → Yes / No
[ ] Error handling differentiated by status code (401, 404, 429, 500) → Yes / No
[ ] Observability for 429 rate, API errors per portal → Yes / No
Real-world scenario: a customer success ISV maps their data model
A customer success product (Pylon-analog) needed to sync ticket data between their app and HubSpot. The mapping decision was the first thing they had to nail.
Their data model:
Tickets (their primary object)
Customer accounts
Conversations
HubSpot's data model for the relevant pieces:
Tickets (HubSpot has these natively, in
/crm/v3/objects/tickets)Companies (HubSpot's "Customer accounts" equivalent)
Conversations (HubSpot has these too, via the Conversations API)
The mapping: → Their Tickets ↔ HubSpot Tickets (1:1, easy) → Their Customers ↔ HubSpot Companies (1:1 by domain match) → Their Conversations ↔ HubSpot Conversations (1:1, but requires Conversations API not CRM API)
The integration ended up using two APIs (CRM v3 for tickets and companies; Conversations for conversations). A single API would have forced ugly workarounds. Right tool for each job.
Frequently Asked Questions
What HubSpot API should I use for CRM integration?
The CRM API v3. It covers contacts, companies, deals, tickets, line items, quotes, products, and custom objects. For 80% of SaaS-to-HubSpot integrations, the CRM API is the right starting point.
How do I authenticate with the HubSpot API?
OAuth for Marketplace apps and multi-tenant integrations, Private App tokens for internal/single-portal integrations. The legacy API key is deprecated and shouldn't be used for new builds (HubSpot intro to auth).
How does HubSpot API pagination work?
HubSpot's v3 API uses cursor pagination. The response includes paging.next.after if more results exist. Pass that value as the after query parameter on the next request. This is not offset-based pagination.
What's the HubSpot batch API endpoint?
HubSpot exposes batch endpoints at /crm/v3/objects/{type}/batch/read, /batch/create, /batch/update, and /batch/archive. Each accepts up to 100 records per request and counts as 1 API call against your rate limit. Use these whenever possible.
How do I get a custom property value via the HubSpot API?
Custom properties work the same as standard properties. Include the property internal name in the properties query parameter when reading, or in the properties field of the JSON body when writing. To discover available custom properties, call /crm/v3/properties/{objectType}.
Does HubSpot support GraphQL?
HubSpot offers a CRM GraphQL API in 2026 for relationship-heavy queries. It complements the REST CRM API; it doesn't replace it. For most simple CRUD operations, REST is still the right choice.
What's the difference between properties and associations in HubSpot?
Properties are fields on an object (e.g., a contact's email address). Associations are links between objects (e.g., a contact is associated with a deal). Both are core CRM concepts and have separate API endpoints.
About the author
Sunny Chauhan is the founder and CEO of Appnigma AI, a no-code platform that generates Salesforce AppExchange-ready Managed Packages from natural-language prompts. He holds Salesforce certifications in Platform Developer II, Platform App Builder, Administrator, Data Cloud Consultant, and AI Associate. Appnigma generates Apex code that handles Salesforce CRM CRUD against the Salesforce REST API; the architectural patterns transfer to HubSpot CRM API integrations.
Originally published June 18, 2026. Last reviewed June 18, 2026. API endpoints and patterns verified against HubSpot's CRM API v3 documentation current as of the published date.
Related articles
Sources
1/ HubSpot Developers, CRM API Reference 2/ HubSpot Developers, Batch API Endpoints 3/ HubSpot Developers, OAuth Quickstart 4/ HubSpot Developers, API Usage and Rate Limits
What's the first API call you're going to write, and which 5 error codes do you have a plan for?
Ready to transform your Salesforce experience?
Start exploring the Salesforce Exchange today and discover apps that can take your CRM efficiency to the next level.
