TL;DR
The HubSpot API lets developers integrate CRM, marketing, sales, and service workflows programmatically. Use OAuth 2.0 for multi-tenant apps or private app tokens for internal integrations, then work with REST endpoints for contacts, companies, deals, tickets, and other CRM objects. This guide covers authentication, core CRUD operations, associations, webhooks, rate limiting, and production readiness.
Introduction
HubSpot manages over 194,000 customer accounts and billions of CRM records. For developers building CRM integrations, marketing automation, or sales tools, HubSpot API integration is essential for reaching 7 million+ users.
Businesses can lose 15–20 hours each week to manual data entry between systems. A HubSpot integration can automate contact synchronization, deal updates, marketing workflows, and reporting across your stack.
💡 Apidog simplifies API integration testing. Use it to test HubSpot endpoints, validate OAuth flows, inspect webhook payloads, and debug authentication issues in one workspace. You can import API specifications, mock responses, and share test scenarios with your team.
What Is the HubSpot API?
HubSpot provides a RESTful API for accessing CRM data and marketing automation features. Common API use cases include:
- Contacts, companies, deals, tickets, and custom objects
- Marketing emails and landing pages
- Sales pipelines and sequences
- Service tickets and conversations
- Analytics and reporting
- Workflows and automation
- Files and assets
Key Features
| Feature | Description |
|---|---|
| RESTful design | Standard HTTP methods with JSON responses |
| OAuth 2.0 and private apps | Flexible authentication options |
| Webhooks | Real-time notifications for object changes |
| Rate limiting | Tier-based limits of 100–400 requests per second |
| CRM objects | Standard and custom object support |
| Associations | Link records, such as contacts to companies |
| Properties | Custom fields for supported object types |
| Search API | Filtering, pagination, and sorting |
API Architecture Overview
HubSpot uses versioned REST APIs with this base URL:
https://api.hubapi.com/
API Versions Compared
| Version | Status | Authentication | Use case |
|---|---|---|---|
| CRM API v3 | Current | OAuth 2.0, private app | New CRM integrations |
| Automation API v4 | Current | OAuth 2.0, private app | Workflow enrollment |
| Marketing Email API | Current | OAuth 2.0, private app | Email campaigns |
| Contacts API v1 | Deprecated | API key (legacy) | Migrate to v3 |
| Companies API v1 | Deprecated | API key (legacy) | Migrate to v3 |
Important: HubSpot deprecated API key authentication in favor of OAuth 2.0 and private apps. Migrate legacy API-key integrations.
Getting Started: Authentication Setup
Step 1: Create a HubSpot Developer Account
Before calling the API:
- Open the HubSpot Developer Portal.
- Sign in with your HubSpot account or create one.
- In the developer dashboard, open Apps.
- Select Create app.
Step 2: Choose an Authentication Method
| Method | Best for | Security model |
|---|---|---|
| OAuth 2.0 | Multi-tenant apps and public integrations | User-scoped tokens |
| Private app | Internal integrations for one portal | Portal-scoped token |
Step 3: Create a Private App for an Internal Integration
For a single HubSpot portal:
- Go to Settings → Integrations → Private Apps.
- Click Create a private app.
- Configure the scopes your integration needs, for example:
contacts
crm.objects.companies
crm.objects.deals
crm.objects.tickets
automation
webhooks
Store the generated token outside source control:
# .env
HUBSPOT_ACCESS_TOKEN="pat-na1-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
HUBSPOT_PORTAL_ID="12345678"
Step 4: Configure OAuth 2.0 for a Multi-Tenant App
Use OAuth when customers install your app in their own HubSpot portals.
const HUBSPOT_CLIENT_ID = process.env.HUBSPOT_CLIENT_ID;
const HUBSPOT_CLIENT_SECRET = process.env.HUBSPOT_CLIENT_SECRET;
const HUBSPOT_REDIRECT_URI = process.env.HUBSPOT_REDIRECT_URI;
const getAuthUrl = (state) => {
const params = new URLSearchParams({
client_id: HUBSPOT_CLIENT_ID,
redirect_uri: HUBSPOT_REDIRECT_URI,
scope: 'crm.objects.contacts.read crm.objects.contacts.write',
state,
optional_scope: 'crm.objects.deals.read'
});
return `https://app.hubspot.com/oauth/authorize?${params.toString()}`;
};
Redirect the user to the generated URL, then handle the authorization code returned to your callback URL.
Step 5: Exchange the Authorization Code for Tokens
const exchangeCodeForToken = async (code) => {
const response = await fetch('https://api.hubapi.com/oauth/v1/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: HUBSPOT_CLIENT_ID,
client_secret: HUBSPOT_CLIENT_SECRET,
redirect_uri: HUBSPOT_REDIRECT_URI,
code
})
});
if (!response.ok) {
throw new Error(`Token exchange failed: ${response.status}`);
}
const data = await response.json();
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresIn: data.expires_in,
portalId: data.hub_portal_id
};
};
app.get('/oauth/callback', async (req, res) => {
const { code } = req.query;
try {
const tokens = await exchangeCodeForToken(code);
await db.installations.create({
portalId: tokens.portalId,
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
tokenExpiry: Date.now() + tokens.expiresIn * 1000
});
res.redirect('/success');
} catch (error) {
console.error('OAuth error:', error);
res.status(500).send('Authentication failed');
}
});
Step 6: Refresh Expiring Access Tokens
HubSpot OAuth access tokens expire after 6 hours. Refresh them before making calls with an expired token.
const refreshAccessToken = async (refreshToken) => {
const response = await fetch('https://api.hubapi.com/oauth/v1/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: HUBSPOT_CLIENT_ID,
client_secret: HUBSPOT_CLIENT_SECRET,
refresh_token: refreshToken
})
});
if (!response.ok) {
throw new Error(`Token refresh failed: ${response.status}`);
}
const data = await response.json();
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresIn: data.expires_in
};
};
const ensureValidToken = async (portalId) => {
const installation = await db.installations.findByPortalId(portalId);
// Refresh when fewer than 30 minutes remain.
if (installation.tokenExpiry < Date.now() + 1_800_000) {
const newTokens = await refreshAccessToken(installation.refreshToken);
await db.installations.update(installation.id, {
accessToken: newTokens.accessToken,
refreshToken: newTokens.refreshToken,
tokenExpiry: Date.now() + newTokens.expiresIn * 1000
});
return newTokens.accessToken;
}
return installation.accessToken;
};
Always persist the latest refresh token returned by HubSpot.
Step 7: Create a Reusable API Client
Centralize authentication and error handling instead of adding headers to every request.
const HUBSPOT_BASE_URL = 'https://api.hubapi.com';
const hubspotRequest = async (endpoint, options = {}, portalId = null) => {
const accessToken = portalId
? await ensureValidToken(portalId)
: process.env.HUBSPOT_ACCESS_TOKEN;
const response = await fetch(`${HUBSPOT_BASE_URL}${endpoint}`, {
...options,
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
...options.headers
}
});
const body = await response.json();
if (!response.ok) {
throw new Error(`HubSpot API Error (${response.status}): ${body.message}`);
}
return body;
};
// Example
const contacts = await hubspotRequest('/crm/v3/objects/contacts');
Working with CRM Objects
Create a Contact
Create a contact by sending its properties to the contacts endpoint.
const createContact = async (contactData) => {
return hubspotRequest('/crm/v3/objects/contacts', {
method: 'POST',
body: JSON.stringify({
properties: {
email: contactData.email,
firstname: contactData.firstName,
lastname: contactData.lastName,
phone: contactData.phone,
company: contactData.company,
website: contactData.website,
lifecyclestage: contactData.lifecycleStage || 'lead'
}
})
});
};
const contact = await createContact({
email: 'john.doe@example.com',
firstName: 'John',
lastName: 'Doe',
phone: '+1-555-0123',
company: 'Acme Corp',
lifecycleStage: 'customer'
});
console.log(`Contact created: ${contact.id}`);
Common Contact Properties
| Property | Type | Description |
|---|---|---|
email |
String | Primary email and unique identifier |
firstname |
String | First name |
lastname |
String | Last name |
phone |
String | Phone number |
company |
String | Company name |
website |
String | Website URL |
lifecyclestage |
Enum |
lead, marketingqualifiedlead, salesqualifiedlead, opportunity, customer, evangelist, subscriber
|
createdate |
DateTime | Automatically generated |
lastmodifieddate |
DateTime | Automatically generated |
Get a Contact by ID
const getContact = async (contactId) => {
return hubspotRequest(`/crm/v3/objects/contacts/${contactId}`);
};
const contact = await getContact('12345');
console.log(`${contact.properties.firstname} ${contact.properties.lastname}`);
console.log(`Email: ${contact.properties.email}`);
Search Contacts
Use the search endpoint when you need filtered results instead of retrieving records one at a time.
const searchContacts = async (filterGroups) => {
return hubspotRequest('/crm/v3/objects/contacts/search', {
method: 'POST',
body: JSON.stringify({
filterGroups,
properties: ['firstname', 'lastname', 'email', 'company'],
limit: 100
})
});
};
const results = await searchContacts([
{
filters: [
{
propertyName: 'company',
operator: 'EQ',
value: 'Acme Corp'
}
]
}
]);
results.results.forEach((contact) => {
console.log(contact.properties.email);
});
Search Filter Operators
| Operator | Description | Example |
|---|---|---|
EQ |
Equal to | company EQ 'Acme' |
NEQ |
Not equal to | lifecyclestage NEQ 'subscriber' |
CONTAINS_TOKEN |
Contains | email CONTAINS_TOKEN 'gmail' |
NOT_CONTAINS_TOKEN |
Does not contain | email NOT_CONTAINS_TOKEN 'test' |
GT |
Greater than | createdate GT '2026-01-01' |
LT |
Less than | createdate LT '2026-12-31' |
GTE |
Greater than or equal | deal_amount GTE 10000 |
LTE |
Less than or equal | deal_amount LTE 50000 |
HAS_PROPERTY |
Has a value | phone HAS_PROPERTY |
NOT_HAS_PROPERTY |
Missing a value | phone NOT_HAS_PROPERTY |
Create a Company
const createCompany = async (companyData) => {
return hubspotRequest('/crm/v3/objects/companies', {
method: 'POST',
body: JSON.stringify({
properties: {
name: companyData.name,
domain: companyData.domain,
industry: companyData.industry,
numberofemployees: companyData.employees,
annualrevenue: companyData.revenue,
city: companyData.city,
state: companyData.state,
country: companyData.country
}
})
});
};
const company = await createCompany({
name: 'Acme Corporation',
domain: 'acme.com',
industry: 'Technology',
employees: 500,
revenue: 50000000,
city: 'San Francisco',
state: 'CA',
country: 'USA'
});
Associate a Contact with a Company
Create associations after creating the corresponding records.
const associateContactWithCompany = async (contactId, companyId) => {
return hubspotRequest(
`/crm/v3/objects/contacts/${contactId}/associations/companies/${companyId}`,
{
method: 'PUT',
body: JSON.stringify({
types: [
{
associationCategory: 'HUBSPOT_DEFINED',
associationTypeId: 1
}
]
})
}
);
};
await associateContactWithCompany('12345', '67890');
Common Association Types
| Association | Type ID | Direction |
|---|---|---|
| Contact → Company | 1 | Contact is associated with company |
| Company → Contact | 1 | Company has associated contact |
| Deal → Contact | 3 | Deal is associated with contact |
| Deal → Company | 5 | Deal is associated with company |
| Ticket → Contact | 16 | Ticket is associated with contact |
| Ticket → Company | 15 | Ticket is associated with company |
Create a Deal
const createDeal = async (dealData) => {
return hubspotRequest('/crm/v3/objects/deals', {
method: 'POST',
body: JSON.stringify({
properties: {
dealname: dealData.name,
amount: dealData.amount.toString(),
dealstage: dealData.stage || 'appointmentscheduled',
pipeline: dealData.pipelineId || 'default',
closedate: dealData.closeDate,
dealtype: dealData.type || 'newbusiness',
description: dealData.description
}
})
});
};
const deal = await createDeal({
name: 'Acme Corp - Enterprise License',
amount: 50000,
stage: 'qualification',
closeDate: '2026-06-30',
type: 'newbusiness',
description: 'Enterprise annual subscription'
});
Associate the new deal with its company and contact:
await hubspotRequest(
`/crm/v3/objects/deals/${deal.id}/associations/companies/${companyId}`,
{
method: 'PUT',
body: JSON.stringify({
types: [
{
associationCategory: 'HUBSPOT_DEFINED',
associationTypeId: 5
}
]
})
}
);
await hubspotRequest(
`/crm/v3/objects/deals/${deal.id}/associations/contacts/${contactId}`,
{
method: 'PUT',
body: JSON.stringify({
types: [
{
associationCategory: 'HUBSPOT_DEFINED',
associationTypeId: 3
}
]
})
}
);
Default Deal Pipeline Stages
| Stage | Internal value |
|---|---|
| Appointments Scheduled | appointmentscheduled |
| Qualified to Buy | qualifiedtobuy |
| Presentation Scheduled | presentationscheduled |
| Decision Maker Bought-In | decisionmakerboughtin |
| Contract Sent | contractsent |
| Closed Won | closedwon |
| Closed Lost | closedlost |
Webhooks
Webhooks let your integration react to record changes without polling the CRM API.
Configure Webhooks
const createWebhook = async (webhookData) => {
return hubspotRequest('/webhooks/v3/my-app/webhooks', {
method: 'POST',
body: JSON.stringify({
webhookUrl: webhookData.url,
eventTypes: webhookData.events,
objectType: webhookData.objectType,
propertyName: webhookData.propertyName
})
});
};
const webhook = await createWebhook({
url: 'https://myapp.com/webhooks/hubspot',
events: [
'contact.creation',
'contact.propertyChange',
'company.creation',
'deal.creation',
'deal.stageChange'
],
objectType: 'contact'
});
console.log(`Webhook created: ${webhook.id}`);
Webhook Event Types
| Event type | Trigger |
|---|---|
contact.creation |
New contact created |
contact.propertyChange |
Contact property updated |
contact.deletion |
Contact deleted |
company.creation |
New company created |
company.propertyChange |
Company property updated |
deal.creation |
New deal created |
deal.stageChange |
Deal stage changed |
deal.propertyChange |
Deal property updated |
ticket.creation |
New ticket created |
ticket.propertyChange |
Ticket property updated |
Handle Webhook Events
Your webhook endpoint should validate incoming requests before processing events.
const express = require('express');
const crypto = require('crypto');
const app = express();
app.post('/webhooks/hubspot', express.json(), async (req, res) => {
const signature = req.headers['x-hubspot-signature'];
const payload = JSON.stringify(req.body);
const isValid = verifyWebhookSignature(
payload,
signature,
process.env.HUBSPOT_CLIENT_SECRET
);
if (!isValid) {
console.error('Invalid webhook signature');
return res.status(401).send('Unauthorized');
}
for (const event of req.body) {
console.log(`Event: ${event.eventType}`);
console.log(`Object: ${event.objectType} - ${event.objectId}`);
console.log(`Property: ${event.propertyName}`);
console.log(`Value: ${event.propertyValue}`);
switch (event.eventType) {
case 'contact.creation':
await handleContactCreation(event);
break;
case 'contact.propertyChange':
await handleContactUpdate(event);
break;
case 'deal.stageChange':
await handleDealStageChange(event);
break;
}
}
res.status(200).send('OK');
});
function verifyWebhookSignature(payload, signature, clientSecret) {
const expectedSignature = crypto
.createHmac('sha256', clientSecret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
}
Rate Limiting
Understand Your Limits
HubSpot rate limits vary by subscription tier.
| Tier | Requests/second | Requests/day |
|---|---|---|
| Free/Starter | 100 | 100,000 |
| Professional | 200 | 500,000 |
| Enterprise | 400 | 1,000,000 |
When you exceed a limit, HubSpot returns 429 Too Many Requests.
Monitor Rate-Limit Headers
| Header | Description |
|---|---|
X-HubSpot-RateLimit-Second-Limit |
Maximum requests per second |
X-HubSpot-RateLimit-Second-Remaining |
Remaining requests in the current second |
X-HubSpot-RateLimit-Second-Reset |
Seconds until the second limit resets |
X-HubSpot-RateLimit-Daily-Limit |
Maximum daily requests |
X-HubSpot-RateLimit-Daily-Remaining |
Remaining requests today |
X-HubSpot-RateLimit-Daily-Reset |
Seconds until the daily limit resets |
Add Retry Logic for 429 Responses
Use exponential backoff when a request is rate limited.
const makeRateLimitedRequest = async (
endpoint,
options = {},
maxRetries = 3
) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await hubspotRequest(endpoint, options);
} catch (error) {
const isRateLimited = error.message.includes('429');
if (!isRateLimited || attempt === maxRetries) {
throw error;
}
const delay = 2 ** attempt * 1000;
console.log(`Rate limited. Retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
};
Queue Requests Before Sending Them
For high-volume synchronization jobs, queue requests and stay below your portal's limit.
class HubSpotRateLimiter {
constructor(requestsPerSecond = 90) {
this.queue = [];
this.interval = 1000 / requestsPerSecond;
this.processing = false;
}
async add(requestFn) {
return new Promise((resolve, reject) => {
this.queue.push({ requestFn, resolve, reject });
this.process();
});
}
async process() {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
while (this.queue.length > 0) {
const { requestFn, resolve, reject } = this.queue.shift();
try {
resolve(await requestFn());
} catch (error) {
reject(error);
}
if (this.queue.length > 0) {
await new Promise((resolve) => setTimeout(resolve, this.interval));
}
}
this.processing = false;
}
}
Production Deployment Checklist
Before releasing your integration:
- [ ] Use a private app token or OAuth 2.0 authentication
- [ ] Store tokens in an encrypted database or secret manager
- [ ] Refresh OAuth tokens automatically
- [ ] Add rate limiting, retries, and request queuing
- [ ] Expose webhook endpoints over HTTPS
- [ ] Validate webhook signatures
- [ ] Handle API errors consistently
- [ ] Log API calls and webhook processing
- [ ] Monitor rate-limit usage
- [ ] Create a runbook for common operational issues
Real-World Use Cases
CRM Synchronization
A SaaS company synchronizes customer data between its application and HubSpot:
- Challenge: Manual data entry between systems
- Implementation: Sync records through HubSpot APIs and use webhooks for near-real-time updates
- Result: Zero manual entry and 100% data accuracy
Lead Routing
A marketing agency automates assignment of new leads:
- Challenge: Slow lead response times
- Implementation: Process webhook events and route leads to the appropriate sales representative
- Result: Five-minute response time and a 40% conversion increase
Conclusion
The HubSpot API provides the building blocks for CRM and marketing automation integrations. For a production-ready implementation:
- Use OAuth 2.0 for multi-tenant apps and private apps for internal integrations.
- Account for subscription-based rate limits of 100–400 requests per second.
- Use webhooks for real-time synchronization.
- Model related CRM records with associations.
- Test authentication flows, API requests, and webhook payloads before deployment.
FAQ
How do I authenticate with the HubSpot API?
Use OAuth 2.0 for multi-tenant applications or private apps for single-portal integrations. API key authentication is deprecated.
What are HubSpot rate limits?
Rate limits range from 100 requests per second on Free plans to 400 requests per second on Enterprise plans. Daily limits range from 100,000 to 1 million requests.
How do I create a contact in HubSpot?
Send a POST request to /crm/v3/objects/contacts with a properties object containing fields such as email, firstname, lastname, and custom properties.
Can I create custom properties?
Yes. Use the Properties API to create custom fields for supported object types.
How do HubSpot webhooks work?
Configure webhook subscriptions in your app settings. HubSpot sends POST requests to your endpoint when subscribed events occur.
Top comments (0)