DEV Community

Preecha
Preecha

Posted on

How to Use HubSpot API in 2026

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.

Try Apidog today

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/
Enter fullscreen mode Exit fullscreen mode

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:

  1. Open the HubSpot Developer Portal.
  2. Sign in with your HubSpot account or create one.
  3. In the developer dashboard, open Apps.
  4. 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:

  1. Go to Settings → Integrations → Private Apps.
  2. Click Create a private app.
  3. Configure the scopes your integration needs, for example:
contacts
crm.objects.companies
crm.objects.deals
crm.objects.tickets
automation
webhooks
Enter fullscreen mode Exit fullscreen mode

Store the generated token outside source control:

# .env
HUBSPOT_ACCESS_TOKEN="pat-na1-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
HUBSPOT_PORTAL_ID="12345678"
Enter fullscreen mode Exit fullscreen mode

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()}`;
};
Enter fullscreen mode Exit fullscreen mode

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');
  }
});
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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}`);
Enter fullscreen mode Exit fullscreen mode

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}`);
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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'
});
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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'
});
Enter fullscreen mode Exit fullscreen mode

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
        }
      ]
    })
  }
);
Enter fullscreen mode Exit fullscreen mode

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}`);
Enter fullscreen mode Exit fullscreen mode

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')
  );
}
Enter fullscreen mode Exit fullscreen mode

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));
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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)