TL;DR
The HubSpot API enables developers to integrate with CRM, marketing, sales, and service hubs programmatically. It uses OAuth 2.0 and private app authentication, RESTful endpoints for contacts, companies, deals, tickets, and more, with rate limits based on subscription tier. This guide covers authentication setup, core endpoints, webhooks, and production integration strategies.
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 isn’t optional—it’s essential for reaching 7 million+ users.
Here’s the reality: businesses lose 15-20 hours weekly on manual data entry between systems. A solid HubSpot API integration automates contact synchronization, deal updates, marketing workflows, and reporting across platforms.
What Is the HubSpot API?
HubSpot provides a RESTful API for accessing CRM data and marketing automation features. The API handles:
- 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:
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 deprecated API key authentication in favor of OAuth 2.0 and private apps. Migrate all integrations immediately.
Getting Started: Authentication Setup
Step 1: Create Your HubSpot Developer Account
Before accessing the API:
- Visit the HubSpot Developer Portal
- Sign in with your HubSpot account (or create one)
- Navigate to Apps in the developer dashboard
- Click Create app
Step 2: Choose Authentication Method
HubSpot supports two 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)
Create a private app for single-portal access:
- Go to Settings > Integrations > Private Apps
- Click Create a private app
- Configure scopes:
contacts
crm.objects.companies
crm.objects.deals
crm.objects.tickets
automation
webhooks
- Generate access token
- Copy and store securely
# .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 multi-portal access:
- Go to Apps > Create app
- Configure auth settings:
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 OAuth callback:
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
};
};
// Handle callback
app.get('/oauth/callback', async (req, res) => {
const { code, state } = req.query;
try {
const tokens = await exchangeCodeForToken(code);
// Store tokens in database
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
Access tokens expire after 6 hours:
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 save new refresh token
expiresIn: data.expires_in
};
};
// Middleware to ensure valid token
const ensureValidToken = async (portalId) => {
const installation = await db.installations.findByPortalId(portalId);
// Refresh if 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 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
const contacts = await hubspotRequest('/crm/v3/objects/contacts');
Working with CRM Objects
Creating a Contact
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 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
Search with filters:
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;
};
// Usage - Find contacts at 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 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 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 by property change
})
});
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
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 based on subscription tier:
| Tier | Requests/Second | Requests/Day |
|---|---|---|
| Free/Starter | 100 | 100,000 |
| Professional | 200 | 500,000 |
| Enterprise | 400 | 1,000,000 |
Exceeding limits results in HTTP 429 (Too Many Requests) responses.
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
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:
- [ ] Use private app or OAuth 2.0 authentication
- [ ] Store tokens securely (encrypted database)
- [ ] Implement automatic token refresh
- [ ] Set up rate limiting and request queuing
- [ ] Configure webhook endpoint with HTTPS
- [ ] Implement comprehensive error handling
- [ ] Add logging for all API calls
- [ ] Monitor rate limit usage
- [ ] Create 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 provides comprehensive CRM and marketing automation capabilities. Key takeaways:
- Use OAuth 2.0 for multi-tenant apps, private apps for internal integrations
- Rate limits vary by tier (100-400 requests/second)
- Webhooks enable real-time synchronization
- CRM objects support associations and custom properties
- Apidog streamlines API testing and team 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?
Rate limits range from 100 requests/second (Free) to 400 requests/second (Enterprise), with daily limits from 100K to 1M requests.
How do I create a contact in HubSpot?
POST to /crm/v3/objects/contacts with properties including email, firstname, lastname, and any custom fields.
Can I create custom properties?
Yes, use the Properties API to create custom fields for any object type.
How do webhooks work in HubSpot?
Configure webhooks in your app settings. HubSpot sends POST requests to your endpoint when specified events occur.



