Skip to content

How to Build a HubSpot Integration: 2026 Architecture Guide

A complete architecture guide for building a HubSpot integration — OAuth 2.0, rate limits, cursor pagination, filterGroups, and the build vs. buy decision.

Roopendra Talekar Roopendra Talekar · · 15 min read
How to Build a HubSpot Integration: 2026 Architecture Guide

Building a HubSpot integration means wiring your app into HubSpot's CRM v3 API to read and write contacts, companies, deals, and tickets. It requires implementing OAuth 2.0 with aggressive token refresh, handling cursor-based pagination, respecting strict rate limits, normalizing HubSpot's dynamic properties object, and constructing complex filterGroups for search.

The short answer to "how do I build a HubSpot integration?" is: register a public OAuth app, build a token management system, write a polling or webhook ingestion service, and map HubSpot's custom properties to your database. The long answer involves exponential backoff, cursor pagination, and months of engineering maintenance.

This guide breaks down each of those pieces — the real architecture, the gotchas nobody warns you about, and the strategic decision of whether to build it yourself or buy.

Why Your B2B SaaS Needs a Native HubSpot Integration

Your sales tool does not exist in isolation. It lives inside a stack of 6 to 10 other tools that every sales rep already juggles daily. If your application cannot read from and write to the system of record, it becomes an administrative burden.

HubSpot CRM holds a 5.32% market share in the CRM platforms category, with over 36,400 companies using it worldwide. That might sound small next to Salesforce's 22%, but look at who those customers are: the majority fall in the 20–49 employee and 100–249 employee company size brackets — exactly the mid-market segment where most B2B SaaS companies find their sweet spot.

HubSpot's revenue grew from $883 million in 2020 to over $2.6 billion in 2025, with over 228,000 paying customers across more than 135 countries. The CRM market they operate in is massive: the global CRM software market reached $112.91 billion in 2025 and is projected to hit $262.74 billion by 2032, driving intense demand for reliable data synchronization.

If you're building a sales tool, revenue intelligence platform, customer success product, or marketing automation tool, your prospects will ask about HubSpot in the first demo. Not the second. Not in the follow-up email. The first demo. When a prospect asks if you integrate with HubSpot and you reply, "it's on the roadmap," the evaluation ends. Missing this integration doesn't just cost you one deal — it signals to buyers that you don't understand their stack.

For a deeper look at what else belongs on your integration roadmap, see our guide to the most requested integrations for B2B sales tools.

The Architecture of a HubSpot Integration

A production-grade HubSpot integration has four major subsystems. Skip any of them and you'll be debugging in production within a month.

flowchart TD
    A["Your Application"] --> B["OAuth 2.0<br>Token Manager"]
    B --> C["Rate Limiter<br>+ Retry Queue"]
    C --> D["HubSpot CRM v3 API"]
    D --> E["Response Parser<br>+ Schema Mapper"]
    E --> A
    F["HubSpot Webhooks"] --> G["Signature<br>Verification"]
    G --> H["Event Router<br>+ Enrichment"]
    H --> A

Building a toy integration that pulls a list of contacts via a static API key takes an hour. Building a multi-tenant, customer-facing integration that handles thousands of connected accounts requires serious architectural planning across each of these subsystems.

OAuth 2.0: 30-Minute Tokens and the Refresh Race

HubSpot uses the OAuth 2.0 authorization code flow for public apps. You register a public app in their developer portal, define your scopes (like crm.objects.contacts.read), and redirect users through the standard authorization flow. You cannot ask your customers to generate and paste static API keys.

Here's the full authorization sequence:

sequenceDiagram
    participant User
    participant App as Your Application
    participant Auth as HubSpot Auth Server
    participant API as HubSpot CRM API

    User->>App: Clicks "Connect HubSpot"
    App->>Auth: Redirects with client_id, scopes, state
    Auth->>User: Prompts for authorization
    User->>Auth: Approves access
    Auth->>App: Redirects back with authorization code
    App->>Auth: POST /oauth/v3/token (exchanges code)
    Auth->>App: Returns access_token & refresh_token
    App->>API: GET /crm/v3/objects/contacts (Bearer token)
    API->>App: Returns Contact Data

Here's where it gets painful. HubSpot changed OAuth access token expiration from 6 hours to 30 minutes. That's an aggressive TTL. The response gives you an access token, a refresh token, and an expires_in value of 1800 seconds.

The good news: refresh tokens stay valid indefinitely unless the user uninstalls your app or you revoke them manually. The bad news: that 30-minute window means your integration must proactively refresh tokens before they expire, not after a 401 Unauthorized hits you in production. Waiting for a 401 to trigger a refresh introduces latency and race conditions when multiple background workers attempt to refresh the same token simultaneously, a distributed systems challenge we cover in our guide to scalable OAuth token management.

A basic token refresh flow looks like this:

async function getValidAccessToken(accountId: string): Promise<string> {
  const account = await db.getIntegratedAccount(accountId);
  const tokenAge = Date.now() - account.tokenIssuedAt;
  const bufferMs = 60 * 1000; // refresh 60s before expiry
  
  if (tokenAge < (account.expiresIn * 1000) - bufferMs) {
    return account.accessToken;
  }
 
  const response = await fetch('https://api.hubspot.com/oauth/v3/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      client_id: process.env.HUBSPOT_CLIENT_ID,
      client_secret: process.env.HUBSPOT_CLIENT_SECRET,
      refresh_token: account.refreshToken,
    }),
  });
 
  if (!response.ok) {
    await db.markAccountNeedsReauth(accountId);
    throw new Error('Token refresh failed - user must re-authorize');
  }
 
  const tokens = await response.json();
  await db.updateTokens(accountId, tokens);
  return tokens.access_token;
}

HubSpot released new OAuth v3 API endpoints in January 2026 with enhanced security features. The v1 OAuth endpoints are now deprecated but remain operational. New integrations should use v3. Make sure you're POSTing to /oauth/v3/token, not the legacy v1 endpoint.

The 30-minute expiry caught a lot of developers off guard. One developer noted they had logic like Max(0, expires_in - 1 hour), which obviously can't subtract an hour from 30 minutes, causing the code to refresh the token on every event loop. Don't hardcode expiry assumptions — always read the expires_in value from the token response.

Credential Storage

Your OAuth tokens are essentially passwords to your customer's CRM. Encrypt them at rest. At minimum, use envelope encryption with a KMS. Never log access tokens. Never return them in API responses to your frontend.

Handling HubSpot API Rate Limits

HubSpot enforces two independent rate limit ceilings that trip up integration engineers.

Burst limits (per 10-second rolling window):

  • Public OAuth apps: 110 requests every 10 seconds per account
  • Private apps on Professional plans: 190 requests per 10 seconds
  • Enterprise accounts: 190 requests per 10 seconds, with a daily limit increased to 1 million requests per day

Daily limits:

  • Professional: 650,000 requests per day
  • Enterprise: 1 million per day
  • The API Limit Increase capacity pack adds 1 million requests per day to the standard limit

Any app or integration exceeding its rate limits receives a 429 Too Many Requests response for all subsequent API calls. And here's the part most developers miss: requests resulting in an error response shouldn't exceed 5% of your total daily requests. If you plan on listing your app in the HubSpot Marketplace, it must stay under this 5% error threshold to be certified.

The practical problem is that 110 requests per 10 seconds sounds generous until you're syncing contacts for a customer with 500,000 records. At 100 records per page, that's 5,000 API calls just for the initial sync — and you're burning through your burst budget if you're also handling live API requests from your app simultaneously.

A naive implementation will fail the sync job. A production-ready implementation requires:

  • A queueing system: Decouple API requests from user actions.
  • Exponential backoff with jitter: When a 429 occurs, pause the worker, wait for a calculated duration, and retry. Add random jitter to prevent the "thundering herd" problem where multiple workers retry at the exact same millisecond.
  • Circuit breakers: If an endpoint consistently returns 5xx errors or 429s despite backoff, temporarily halt all requests to that endpoint to prevent cascading failures.

Even exponential backoff is not enough when retries happen across threads, tenants, or object types. What looks like a resilient retry strategy often turns into a retry storm. You need a proper token-bucket rate limiter that's shared across all workers hitting the same HubSpot account — not per-thread backoff logic.

class HubSpotRateLimiter {
  private tokens: number;
  private lastRefill: number;
  private readonly maxTokens = 100; // conservative buffer below 110
  private readonly refillInterval = 10_000; // 10 seconds
 
  async acquire(): Promise<void> {
    this.refill();
    if (this.tokens <= 0) {
      const waitMs = this.refillInterval - (Date.now() - this.lastRefill);
      await new Promise(resolve => setTimeout(resolve, waitMs));
      this.refill();
    }
    this.tokens--;
  }
 
  private refill(): void {
    const now = Date.now();
    if (now - this.lastRefill >= this.refillInterval) {
      this.tokens = this.maxTokens;
      this.lastRefill = now;
    }
  }
}

HubSpot's CRM Search endpoint now supports 200 records per call. Use it. Fetching 200 contacts per request instead of 100 cuts your API call volume in half during bulk syncs.

How HubSpot Cursor Pagination Actually Works

HubSpot's CRM v3 API uses cursor-based pagination. You cannot request ?page=5. Every list response includes a paging object with a next.after token that you pass as a query parameter to get the next page.

When you request a list of objects, HubSpot returns a payload like this:

{
  "results": [
    { "id": "123", "properties": { "firstname": "Jane" } },
    { "id": "124", "properties": { "firstname": "Alex" } }
  ],
  "paging": {
    "next": {
      "after": "NTI1Cg%3D%3D",
      "link": "?after=NTI1Cg%3D%3D"
    }
  }
}

The after value is an opaque cursor. You cannot predict it, skip ahead, or jump to a specific page. There's no random access, no previous page cursor, no total count in list responses. Your code must check for the existence of paging.next.after and append it to the next request. This forces your synchronization logic to be strictly sequential — you cannot parallelize fetching page 1, page 2, and page 3.

Here's the basic pagination loop:

async function fetchAllContacts(accessToken: string): Promise<any[]> {
  const contacts: any[] = [];
  let after: string | null = null;
 
  do {
    const params = new URLSearchParams({
      limit: '100',
      properties: 'firstname,lastname,email,phone',
    });
    if (after) params.set('after', after);
 
    const response = await fetch(
      `https://api.hubspot.com/crm/v3/objects/contacts?${params}`,
      { headers: { Authorization: `Bearer ${accessToken}` } }
    );
    const data = await response.json();
    contacts.push(...data.results);
 
    after = data.paging?.next?.after ?? null;
  } while (after);
 
  return contacts;
}

The Search API gotcha: HubSpot's Search endpoint (POST /crm/v3/objects/contacts/search) uses the same cursor pagination, but the after parameter can't be greater than 10,000. That means you can only page through the first 10,000 results of any search query. If you need more, you have to break your query into smaller time windows or use the standard list endpoint with hs_lastmodifieddate filtering.

Warning

Pagination Edge Cases: Cursor tokens can expire, and the format of the cursor changes between different HubSpot API versions (e.g., v1 vs v3). Your pagination logic must be resilient to malformed or expired cursors.

Handling Custom Properties and FilterGroups

This is where most HubSpot integrations get messy.

The Custom Properties Problem

Every HubSpot instance is heavily customized. While standard fields like firstname and email exist, enterprise customers rely on custom properties (e.g., target_account_tier, churn_risk_score). HubSpot doesn't return contact fields as flat, top-level keys. Every field — both default and custom — lives inside a nested properties object:

{
  "id": "512",
  "properties": {
    "createdate": "2026-01-15T10:30:00Z",
    "email": "founder@example.com",
    "firstname": "Jane",
    "lastname": "Doe",
    "hs_additional_emails": "jane.doe@personal.com;jane@startup.io",
    "custom_churn_risk": "High"
  }
}

Notice the hs_additional_emails field. It is a semicolon-separated string, not an array. Your code must split this string and normalize it into a usable format.

You don't get all properties by default — you must explicitly request them via the properties query parameter. Miss a field, and it silently won't appear in the response. Custom properties are especially tricky because each HubSpot account has different ones. Your integration needs to either:

  1. Discover properties dynamically using the Properties API (GET /crm/v3/properties/contacts), then include them in every request
  2. Let customers configure which custom fields they care about, and only fetch those

Searching with FilterGroups

The Search API makes things worse. Filtering contacts requires constructing filterGroups — an array-of-arrays structure that represents boolean logic. Filters within a group are ANDed; groups are ORed.

To search for a contact where the email matches "founder@example.com" AND the lead status is "QUALIFIED":

{
  "filterGroups": [
    {
      "filters": [
        {
          "propertyName": "email",
          "operator": "EQ",
          "value": "founder@example.com"
        },
        {
          "propertyName": "hs_lead_status",
          "operator": "EQ",
          "value": "QUALIFIED"
        }
      ]
    }
  ],
  "limit": 100
}

To search where the first name contains "Jane" OR the email matches "founder@example.com", you need separate filter groups:

{
  "filterGroups": [
    {
      "filters": [
        {
          "propertyName": "firstname",
          "operator": "CONTAINS_TOKEN",
          "value": "Jane"
        }
      ]
    },
    {
      "filters": [
        {
          "propertyName": "email",
          "operator": "EQ",
          "value": "founder@example.com"
        }
      ]
    }
  ]
}

The supported operators vary by field type (EQ, NEQ, LT, GT, CONTAINS_TOKEN, IN, HAS_PROPERTY, etc.), and getting them wrong produces unhelpful error messages. Building a query builder that translates your application's internal search parameters into HubSpot's filterGroups syntax requires significant engineering effort. This is the kind of API surface that looks simple in the docs and takes two weeks to get right across every edge case.

For teams that need to map HubSpot's properties to a standard data model alongside other CRMs, this normalization work multiplies. Salesforce uses flat PascalCase fields (FirstName, LastName), Pipedrive uses a completely different structure, and Zoho has its own conventions. Mapping all of these to one internal schema is the hardest problem in SaaS integrations.

Webhook Ingestion and Bidirectional Sync

Polling HubSpot every 5 minutes to check for updated contacts is highly inefficient and will quickly exhaust your rate limits. Instead, you must register webhook subscriptions. HubSpot sends HTTP POST requests to your server whenever a record is created, updated, or deleted.

Your infrastructure must:

  • Verify the HMAC signature of incoming payloads using your app secret to ensure authenticity
  • Enqueue events asynchronously rather than processing them inline — webhook handlers must return 200 quickly or HubSpot will retry
  • Enrich payloads by fetching the full record from the API, since webhook payloads contain only the object ID and changed properties
  • Handle deduplication — HubSpot may send the same event multiple times, so your processing must be idempotent

Bidirectional sync introduces another challenge: your integration needs to push updates back to HubSpot while also listening to HubSpot webhooks. This requires mapping your internal data model to HubSpot's schema, handling validation errors, and ensuring that updates don't trigger infinite loops. Without careful tracking of which changes originated from your app, every write triggers a webhook, which triggers another write, ad infinitum. For a deep dive into this problem, see our guide on how to architect a bidirectional HubSpot sync without infinite loops.

Build vs. Buy: The True Cost of Engineering a HubSpot Integration

Let's be honest about the math.

When product managers ask for a HubSpot integration, developers usually look at the API documentation, find the /crm/v3/objects/contacts endpoint, and estimate a week of work. This is the integration iceberg. The HTTP request is the 10% visible above the water. The remaining 90% — authentication, pagination, rate limiting, data normalization, webhooks, and ongoing maintenance — lurks below the surface.

Building in-house for a single HubSpot integration means:

Component Estimated Engineering Time
OAuth 2.0 flow + token refresh 1–2 weeks
Cursor pagination + rate limiting 1 week
Data normalization (contacts, companies, deals) 2–3 weeks
Webhook ingestion + signature verification 1–2 weeks
Custom field discovery + mapping 1–2 weeks
Error handling, retry logic, monitoring 1–2 weeks
Total initial build 7–12 weeks

That's before ongoing maintenance. HubSpot changes things. They moved token expiry from 6 hours to 30 minutes with minimal notice. They introduced new OAuth v3 endpoints and deprecated v1. They adjust rate limits. Every change requires engineering time to detect, understand, and patch — forever.

If HubSpot is truly the only CRM your customers will ever use, building in-house can make sense. You get full control, no third-party dependency, and you can optimize the integration for your exact use case.

But if your roadmap includes Salesforce, Pipedrive, Zoho, Dynamics 365, or any combination of CRMs — and it will, because prospects always ask — then you're signing up to repeat this entire exercise for each one. Every hour your senior engineers spend maintaining a HubSpot integration is an hour they're not building your core product features. The true cost of building integrations in-house scales linearly with every new CRM you add.

How Truto Handles the HubSpot Integration

Truto takes a fundamentally different approach: every integration — HubSpot included — is defined as declarative configuration, not code. There are no if (provider === 'hubspot') branches in the codebase. The same execution engine that calls HubSpot's API also calls Salesforce, Pipedrive, and 100+ other integrations, driven entirely by JSON config and JSONata transformation expressions.

OAuth, Rate Limits, and Pagination — Handled by Config

HubSpot's OAuth flow, 30-minute token expiry, and proactive refresh are all managed through a declarative authentication configuration. Truto refreshes OAuth tokens shortly before they expire, and if a refresh fails, the connected account is automatically flagged as needs_reauth with a webhook event fired to your app.

Rate limiting is configured per integration. When HubSpot returns a 429, Truto detects it and applies backoff automatically — your app never sees the error. Cursor pagination through paging.next.after is handled by the platform's cursor pagination strategy, configured once in the HubSpot integration definition.

Schema Normalization via JSONata

The mapping between HubSpot's nested properties object and Truto's unified CRM schema is a JSONata expression. HubSpot's properties.firstname maps to the unified first_name. The semicolon-delimited hs_additional_emails field gets split into an array. Six different phone fields collapse into a typed phone_numbers array. All of this happens in a declarative mapping that can be modified without redeploying anything.

Your app calls one endpoint:

GET /unified/crm/contacts?integrated_account_id=abc123&limit=10

And gets back a normalized response — the same shape whether the underlying CRM is HubSpot, Salesforce, or Pipedrive:

{
  "result": [
    {
      "id": "123",
      "first_name": "Jane",
      "last_name": "Smith",
      "email_addresses": [
        { "email": "jane@example.com", "is_primary": true },
        { "email": "jane.smith@work.com" }
      ],
      "phone_numbers": [
        { "number": "+1-555-0123", "type": "phone" },
        { "number": "+1-555-0456", "type": "mobile" }
      ],
      "custom_fields": { "custom_field_abc": "some value" },
      "remote_data": { /* original HubSpot response preserved */ }
    }
  ],
  "next_cursor": "..."
}

The remote_data field preserves the original HubSpot response, so you always have access to raw data that the unified schema doesn't cover.

Dynamic Resource Routing

HubSpot's "list contacts" operation actually routes to different endpoints depending on the query. A plain list goes to the standard contacts endpoint. Adding filter parameters routes to the search endpoint with filterGroups. Specifying a view ID routes to a third endpoint for list results. Truto handles this with dynamic resource resolution — a JSONata expression evaluates the incoming query and picks the right endpoint, all configured as data. You never have to think about which HubSpot endpoint to call.

Per-Customer Customization Without Code

Every enterprise customer's HubSpot instance is different. Custom properties, custom objects, unique workflows. Truto's three-level override system (platform default, environment override, per-account override) means you can customize how mappings work for specific customers without changing the base integration. If one customer needs a special custom field included in the unified response, you override the response mapping for just that account.

Built-in MCP Tool Generation

Truto automatically exposes HubSpot resources as AI agent tools with generated JSON schemas, bypassing the need to write custom wrappers for LLM function calling. If your product involves AI agents that need CRM context, this eliminates an entire layer of integration glue code.

Trade-offs to Consider

Unified APIs are not free lunches. You're adding a dependency between your app and a third-party service. The unified schema necessarily loses some HubSpot-specific nuance — though remote_data and the Proxy API give you escape hatches for direct access when you need it. If you have very HubSpot-specific workflow requirements that go beyond CRUD operations, you may still need the Proxy API for those edge cases.

The honest calculation: if your team needs HubSpot plus two or more other CRMs, the unified API approach pays for itself in weeks, not months. If you only need HubSpot and have engineering bandwidth to spare, building in-house gives you more control.

Your Integration Roadmap: What to Do Next

Here's the honest sequence for a PM staring at a spreadsheet of lost deals:

  1. Audit your pipeline. Count the deals blocked by missing HubSpot (and CRM) integrations. Put a dollar figure on it. This is your business case.

  2. Decide build vs. buy based on scope. If HubSpot is the only integration you'll need for 12+ months, consider building it. If your roadmap has 3+ CRMs, the math favors a unified API every time.

  3. Start with read operations. Contacts and companies. Get data flowing into your app. This alone unblocks most sales conversations.

  4. Add write-back and webhooks. Bidirectional sync is where the real product stickiness comes from. Prospects want to see their CRM update in real time when they use your tool, which requires architecting real-time CRM syncs that can handle rate limits and conflicting writes.

  5. Plan for the long tail. HubSpot is one CRM. Your enterprise prospects use Salesforce. Your SMB customers use Pipedrive or Zoho. Build your architecture assuming you'll support all of them — whether that means a unified API or a well-abstracted internal integration layer. The startup integration playbook covers how to think about this strategically.

The CRM market is headed toward $262 billion by 2032. Your product either plugs into this ecosystem or gets left out of the buying conversation entirely. The question isn't whether to build the HubSpot integration — it's how fast you can ship it without letting integration work eat your roadmap alive.

FAQ

How long do HubSpot OAuth access tokens last?
HubSpot OAuth access tokens expire 30 minutes after being generated. Refresh tokens do not expire unless the user uninstalls your app. You need proactive token refresh logic — not reactive 401-triggered refreshes — to avoid failures in production.
What are HubSpot's API rate limits for public apps?
Public OAuth apps on HubSpot are limited to 110 requests per 10 seconds per account. Private apps on Professional plans get 190 requests per 10 seconds, and Enterprise plans also get 190 per 10 seconds with a higher daily cap of 1 million requests. Error responses must stay under 5% for Marketplace certification.
How does HubSpot API pagination work?
HubSpot uses cursor-based pagination. Each response includes a paging.next.after token that must be passed as the 'after' query parameter in the next request. You cannot skip pages or jump to a specific offset. The Search API has a hard limit of 10,000 results per query.
How long does it take to build a HubSpot integration?
A basic read-only HubSpot integration takes 1-2 weeks. A production-grade integration with bidirectional sync, webhook handling, rate limit management, and custom field support typically takes 7-12 weeks of engineering time, plus ongoing maintenance.
Should I build a HubSpot integration in-house or use a unified API?
If HubSpot is the only CRM you'll ever support, building in-house gives you full control. If your roadmap includes Salesforce, Pipedrive, Zoho, or any combination of CRMs, a unified API eliminates the per-integration maintenance burden and lets you ship all of them through a single standardized interface.

More from our Blog