TL;DR
The HubSpot API lets you integrate programmatically with HubSpot’s CRM, marketing, sales, and service hubs. It supports OAuth 2.0 and private app authentication, exposes RESTful endpoints for contacts, companies, deals, tickets, and more, and enforces rate limits by subscription tier. This guide shows you how to set up authentication, use core endpoints, configure webhooks, and build robust production integrations.
Introduction
HubSpot manages 194,000+ customer accounts and billions of CRM records. If you’re building CRM integrations, marketing automation, or sales tools, HubSpot API integration is essential for accessing 7 million+ users.
Manual data entry between systems costs businesses 15-20 hours weekly. With HubSpot API, automate contact sync, deal updates, marketing workflow triggers, and cross-platform reporting.
💡 Apidog simplifies API integration testing: Test HubSpot endpoints, validate OAuth, inspect webhook payloads, and debug authentication in one workspace. Import API specs, mock responses, and share test scenarios with your team.
What Is the HubSpot API?
HubSpot’s RESTful API gives you access to CRM data and marketing automation. It covers:
- 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 + Private Apps | Flexible authentication options |
| Webhooks | Real-time notifications for object changes |
| Rate Limiting | Tier-based limits (100-400 requests/second) |
| CRM Objects | Standard and custom object support |
| Associations | Link objects together (contact-company, deal-contact) |
| Properties | Custom fields for any object type |
| Search API | Complex filtering and sorting |
API Architecture Overview
HubSpot uses versioned REST APIs. All endpoints start with:
https://api.hubapi.com/
API Versions Compared
| Version | Status | Authentication | Use Case |
|---|---|---|---|
| CRM API v3 | Current | OAuth 2.0, Private App | All new 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 API key authentication is deprecated. Use OAuth 2.0 or private apps for all new and existing integrations.
Getting Started: Authentication Setup
Step 1: Create Your HubSpot Developer Account
- Go to the HubSpot Developer Portal
- Sign in or create a HubSpot account
- Navigate to Apps in the dashboard
- Click Create app
Step 2: Choose Authentication Method
HubSpot supports these authentication methods:
| Method | Best For | Security Level |
|---|---|---|
| OAuth 2.0 | Multi-tenant apps, public integrations | High (user-scoped tokens) |
| Private App | Internal integrations, single portal | High (portal-scoped token) |
Step 3: Set Up Private App (Recommended for Internal Integrations)
To use a private app:
- Go to Settings > Integrations > Private Apps
- Click Create a private app
-
Select scopes:
contacts crm.objects.companies crm.objects.deals crm.objects.tickets automation webhooks Generate an access token and save it securely
Example .env file:
# .env file
HUBSPOT_ACCESS_TOKEN="pat-na1-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
HUBSPOT_PORTAL_ID="12345678"
Step 4: Set Up OAuth 2.0 (For Multi-Tenant Apps)
Configure OAuth for apps needing access to multiple 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;
// Build authorization URL
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: state,
optional_scope: 'crm.objects.deals.read'
});
return `https://app.hubspot.com/oauth/authorize?${params.toString()}`;
};
Step 5: Exchange Code for Access Token
Handle the OAuth callback to get 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: code
})
});
const data = await response.json();
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresIn: data.expires_in,
portalId: data.hub_portal_id
};
};
// Express route example
app.get('/oauth/callback', async (req, res) => {
const { code, state } = req.query;
try {
const tokens = await exchangeCodeForToken(code);
// Store tokens in your DB
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 Access Token
Tokens expire after 6 hours. Use your refresh token to get new ones:
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
})
});
const data = await response.json();
return {
accessToken: data.access_token,
refreshToken: data.refresh_token, // Always use the latest refresh token
expiresIn: data.expires_in
};
};
// Middleware to ensure valid token
const ensureValidToken = async (portalId) => {
const installation = await db.installations.findByPortalId(portalId);
// Refresh if token expires within 30 minutes
if (installation.tokenExpiry < Date.now() + 1800000) {
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;
};
Step 7: Make Authenticated API Calls
Create a reusable API client:
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
}
});
if (!response.ok) {
const error = await response.json();
throw new Error(`HubSpot API Error: ${error.message}`);
}
return response.json();
};
// Usage example:
const contacts = await hubspotRequest('/crm/v3/objects/contacts');
Working with CRM Objects
Creating a Contact
To create or update a contact:
const createContact = async (contactData) => {
const contact = {
properties: {
email: contactData.email,
firstname: contactData.firstName,
lastname: contactData.lastName,
phone: contactData.phone,
company: contactData.company,
website: contactData.website,
lifecyclestage: contactData.lifecycleStage || 'lead'
}
};
const response = await hubspotRequest('/crm/v3/objects/contacts', {
method: 'POST',
body: JSON.stringify(contact)
});
return response;
};
// Usage
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}`);
Contact Properties
| Property | Type | Description |
|---|---|---|
email |
String | Primary email (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 | Auto-generated |
lastmodifieddate |
DateTime | Auto-generated |
Getting a Contact
Fetch a contact by ID:
const getContact = async (contactId) => {
const response = await hubspotRequest(`/crm/v3/objects/contacts/${contactId}`);
return response;
};
// Usage
const contact = await getContact('12345');
console.log(`${contact.properties.firstname} ${contact.properties.lastname}`);
console.log(`Email: ${contact.properties.email}`);
Searching Contacts
Use filters to find contacts:
const searchContacts = async (searchCriteria) => {
const response = await hubspotRequest('/crm/v3/objects/contacts/search', {
method: 'POST',
body: JSON.stringify({
filterGroups: searchCriteria,
properties: ['firstname', 'lastname', 'email', 'company'],
limit: 100
})
});
return response;
};
// Example: Find contacts at a specific company
const results = await searchContacts({
filterGroups: [
{
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 |
Doesn’t contain | email NOT_CONTAINS_TOKEN 'test' |
GT |
Greater than | createdate GT '2026-01-01' |
LT |
Less than | createdate LT '2026-12-31' |
GTE |
Greater or equal | deal_amount GTE 10000 |
LTE |
Less or equal | deal_amount LTE 50000 |
HAS_PROPERTY |
Has value | phone HAS_PROPERTY |
NOT_HAS_PROPERTY |
Missing value | phone NOT_HAS_PROPERTY |
Creating a Company
Create a company record:
const createCompany = async (companyData) => {
const company = {
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 response = await hubspotRequest('/crm/v3/objects/companies', {
method: 'POST',
body: JSON.stringify(company)
});
return response;
};
// Usage
const company = await createCompany({
name: 'Acme Corporation',
domain: 'acme.com',
industry: 'Technology',
employees: 500,
revenue: 50000000,
city: 'San Francisco',
state: 'CA',
country: 'USA'
});
Associating Objects
Link contacts to companies:
const associateContactWithCompany = async (contactId, companyId) => {
const response = await hubspotRequest(
`/crm/v3/objects/contacts/${contactId}/associations/companies/${companyId}`,
{
method: 'PUT',
body: JSON.stringify({
types: [
{
associationCategory: 'HUBSPOT_DEFINED',
associationTypeId: 1 // Contact to Company
}
]
})
}
);
return response;
};
// Usage
await associateContactWithCompany('12345', '67890');
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 |
Creating a Deal
Create a sales opportunity:
const createDeal = async (dealData) => {
const deal = {
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 response = await hubspotRequest('/crm/v3/objects/deals', {
method: 'POST',
body: JSON.stringify(deal)
});
return response;
};
// Usage
const deal = await createDeal({
name: 'Acme Corp - Enterprise License',
amount: 50000,
stage: 'qualification',
closeDate: '2026-06-30',
type: 'newbusiness',
description: 'Enterprise annual subscription'
});
// Associate with 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 }] }) }
);
Deal Stages (Default Pipeline)
| 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
Configuring Webhooks
Set up webhooks for real-time notifications:
const createWebhook = async (webhookData) => {
const response = await hubspotRequest('/webhooks/v3/my-app/webhooks', {
method: 'POST',
body: JSON.stringify({
webhookUrl: webhookData.url,
eventTypes: webhookData.events,
objectType: webhookData.objectType,
propertyName: webhookData.propertyName // Optional filter
})
});
return response;
};
// Usage
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 |
Handling Webhooks
Example Express handler with signature verification:
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);
// Verify webhook signature
const isValid = verifyWebhookSignature(payload, signature, process.env.HUBSPOT_CLIENT_SECRET);
if (!isValid) {
console.error('Invalid webhook signature');
return res.status(401).send('Unauthorized');
}
const events = req.body;
for (const event of events) {
console.log(`Event: ${event.eventType}`);
console.log(`Object: ${event.objectType} - ${event.objectId}`);
console.log(`Property: ${event.propertyName}`);
console.log(`Value: ${event.propertyValue}`);
// Route to appropriate handler
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
Understanding Rate Limits
HubSpot enforces rate limits by subscription tier:
| Tier | Requests/Second | Requests/Day |
|---|---|---|
| Free/Starter | 100 | 100,000 |
| Professional | 200 | 500,000 |
| Enterprise | 400 | 1,000,000 |
If you exceed the limit, you’ll get HTTP 429 (Too Many Requests).
Rate Limit Headers
| Header | Description |
|---|---|
X-HubSpot-RateLimit-Second-Limit |
Max requests per second |
X-HubSpot-RateLimit-Second-Remaining |
Remaining requests this second |
X-HubSpot-RateLimit-Second-Reset |
Seconds until second limit resets |
X-HubSpot-RateLimit-Daily-Limit |
Max requests per day |
X-HubSpot-RateLimit-Daily-Remaining |
Remaining requests today |
X-HubSpot-RateLimit-Daily-Reset |
Seconds until daily limit resets |
Implementing Rate Limit Handling
Basic retry logic and queueing:
const makeRateLimitedRequest = async (endpoint, options = {}, maxRetries = 3) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await hubspotRequest(endpoint, options);
// Log rate limit info
const remaining = response.headers.get('X-HubSpot-RateLimit-Second-Remaining');
if (remaining < 10) {
console.warn(`Low rate limit remaining: ${remaining}`);
}
return response;
} catch (error) {
if (error.message.includes('429') && attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000;
console.log(`Rate limited. Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
};
// Rate limiter class
class HubSpotRateLimiter {
constructor(requestsPerSecond = 90) { // Stay under limit
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 {
const result = await requestFn();
resolve(result);
} catch (error) {
reject(error);
}
if (this.queue.length > 0) {
await new Promise(r => setTimeout(r, this.interval));
}
}
this.processing = false;
}
}
Production Deployment Checklist
Before going live, ensure you:
- [ ] Use private app or OAuth 2.0 authentication
- [ ] Store tokens securely (encrypted DB)
- [ ] Implement automatic token refresh
- [ ] Set up rate limiting and request queuing
- [ ] Configure webhook endpoints with HTTPS
- [ ] Implement comprehensive error handling
- [ ] Add logging for all API calls
- [ ] Monitor rate limit usage
- [ ] Create a runbook for common issues
Real-World Use Cases
CRM Synchronization
A SaaS company syncs customer data:
- Challenge: Manual data entry between app and HubSpot
- Solution: Real-time sync via webhooks and API
- Result: Zero manual entry, 100% data accuracy
Lead Routing
A marketing agency automates lead distribution:
- Challenge: Slow lead response times
- Solution: Webhook-triggered routing to sales reps
- Result: 5-minute response time, 40% conversion increase
Conclusion
The HubSpot API gives you robust CRM and marketing automation. To implement:
- Use OAuth 2.0 for multi-tenant apps, private apps for internal integrations
- Respect rate limits (100–400 requests/second by tier)
- Use webhooks for real-time sync
- Leverage associations and custom properties for CRM objects
- Use Apidog to streamline API testing and collaboration
FAQ Section
How do I authenticate with HubSpot API?
Use OAuth 2.0 for multi-tenant apps, or private apps for single-portal integrations. API key authentication is deprecated.
What are HubSpot rate limits?
100–400 requests/sec depending on tier, daily limits from 100K to 1M.
How do I create a contact in HubSpot?
POST to /crm/v3/objects/contacts with properties: email, firstname, lastname, and any custom fields.
Can I create custom properties?
Yes. Use the Properties API for custom fields on any object.
How do webhooks work in HubSpot?
Configure webhooks in your app settings. HubSpot sends POSTs to your endpoint when events occur.
Top comments (0)