Claude Code Plugins

Community-maintained marketplace

Feedback

hubspot-nango-integration

@NangoHQ/ai-ide-prompts
0
0

Use when writing HubSpot integration code in Nango - HubSpot-specific guidance on Search API for incremental syncs, property name variations, rate limits, and OAuth introspection

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name hubspot-nango-integration
description Use when writing HubSpot integration code in Nango - HubSpot-specific guidance on Search API for incremental syncs, property name variations, rate limits, and OAuth introspection

HubSpot Integration Specialist for Nango

Use this skill when writing, reviewing, or troubleshooting HubSpot-specific aspects of Nango integrations.

Focus: This skill covers HubSpot API quirks, not general Nango patterns.

Critical HubSpot API Knowledge

Incremental Syncs: Search API is Required

IMPORTANT: HubSpot incremental syncs can ONLY be achieved via the Search API endpoint.

Why: Only the Search API supports filtering by hs_lastmodifieddate (or lastmodifieddate for contacts), which is essential for incremental syncs.

Trade-off: The Search API has a lower rate limit than other endpoints:

  • Search API Rate Limit: 4 requests per second per authentication token
  • Standard API Rate Limit: 190 calls per 10 seconds (up to 250 with capacity pack)

Implication: When designing HubSpot integrations, you must balance:

  • Incremental sync efficiency (Search API required)
  • Rate limit constraints (Search API is more restrictive)

Search API Endpoint Pattern

// POST /crm/v3/objects/{object_type}/search
// Examples:
// POST /crm/v3/objects/contacts/search
// POST /crm/v3/objects/companies/search
// POST /crm/v3/objects/deals/search

Filtering by Last Modified Date

Property Name Varies by Object:

  • Contacts: Use lastmodifieddate (not hs_lastmodifieddate)
  • Companies, Deals, Tasks, etc.: Use hs_lastmodifieddate

Date Format: UNIX timestamp in milliseconds

Example Request Body:

{
  "filterGroups": [{
    "filters": [{
      "propertyName": "hs_lastmodifieddate",
      "operator": "GTE",
      "value": "1579514400000"
    }]
  }],
  "properties": ["id", "name", "hs_lastmodifieddate"],
  "limit": 100,
  "after": "cursor_token_here"
}

Supported Operators:

  • GTE (greater than or equal)
  • GT (greater than)
  • LTE (less than or equal)
  • LT (less than)
  • BETWEEN (requires both value and highValue)

Search API Limitations

  • Maximum Results: 10,000 total results per search
  • Maximum Filter Groups: 5
  • Maximum Filters Total: 25 in a single query
  • Maximum Filters per Group: 10

OAuth Token Introspection Endpoints

HubSpot provides OAuth token introspection endpoints to validate tokens and retrieve associated user/account information:

Endpoints:

  • Access Tokens: /oauth/v1/access-tokens/:token
  • Refresh Tokens: /oauth/v1/refresh-tokens/:token

Use Cases:

  • Validate OAuth tokens programmatically
  • Retrieve user_id associated with a token
  • Check token expiration and scopes
  • Gather account metadata for debugging

Example Usage in Nango:

// Validate and inspect refresh token
const response = await nango.get({
  endpoint: `/oauth/v1/refresh-tokens/${refreshToken}`,
  baseUrlOverride: 'https://api.hubapi.com'
});

const { user_id, hub_id, scopes } = response.data;

Schema Introspection Pattern

HubSpot provides powerful schema introspection endpoints that allow you to discover:

  • Custom object definitions
  • Standard object properties (including custom fields)
  • Associations between objects
  • Field types and configurations

This is critical for building flexible integrations that adapt to customer-specific customizations.

Schema Introspection Endpoints

1. List All Schemas (Custom + Standard):

GET /crm-object-schemas/v3/schemas

Returns all custom object schemas. Each schema includes properties, associations, and metadata.

2. Get Specific Object Schema:

GET /crm-object-schemas/v3/schemas/{objectTypeId}

Returns detailed schema for a specific object (works for both custom and standard objects).

Required Scope: crm.schemas.custom.read

Schema Response Structure

interface HubSpotSchemaResult {
  id: string                        // Object type ID (e.g., "0-1" for Contact)
  name: string                      // Object name
  objectTypeId: string              // Same as id
  primaryDisplayProperty: string    // Which property to use as display name
  properties: HubSpotProperty[]     // All properties including custom fields
  associations: HubSpotAssociation[] // All associations
}

interface HubSpotProperty {
  name: string              // Property ID (e.g., "firstname", "custom_field_1")
  label: string             // Display label
  type: string              // HubSpot type (string, number, date, enumeration, etc.)
  fieldType: string         // Field type (text, textarea, select, etc.)
  options?: Array<{         // For enumeration/select fields
    label: string
    value: string
  }>
}

interface HubSpotAssociation {
  id: string                // Association ID
  name: string              // Association name
  fromObjectTypeId: string  // Source object
  toObjectTypeId: string    // Target object
}

Conceptual Pattern: Two-Phase Sync

Advanced HubSpot integrations use a two-phase sync pattern:

Phase 1: Schema Sync - Discover structure

  • Fetch all schemas via introspection endpoints
  • Identify custom objects and standard objects
  • Map field types and associations
  • Cache schema information in metadata

Phase 2: Records Sync - Fetch data

  • Use cached schema to know which properties exist
  • Dynamically build property lists based on schema
  • Handle associations discovered in schema phase
  • Adapt to customer-specific customizations without code changes

Why This Matters

Without Introspection:

// Hardcoded - breaks if customer adds custom fields
const properties = ['firstname', 'lastname', 'email'];

With Introspection:

// Dynamic - adapts to customer's schema
const schema = await getSchema(nango, objectId);
const properties = schema.properties.map(p => p.name);
// Includes all custom fields automatically!

HubSpot Object Type IDs

Standard Objects have numeric IDs:

  • 0-1 - Contact
  • 0-2 - Company
  • 0-3 - Deal
  • 0-5 - Ticket
  • 0-7 - Product
  • 0-8 - Line Item
  • 0-136 - Lead
  • owner - Owner (special case)

Custom Objects have different ID formats (provided by HubSpot when created).

Handling Custom Fields

Key Insight: HubSpot custom fields are just additional properties in the schema. They appear alongside standard fields.

Standard Contact Fields:

  • firstname, lastname, email (built-in)

Custom Contact Fields (examples):

  • custom_field_name (user-defined)
  • department, employee_id, etc.

All appear in the same properties array from schema introspection.

// Fetch schema for contacts
const schema = await nango.get({
  endpoint: '/crm-object-schemas/v3/schemas/0-1', // Contact
  retries: 10
});

// Properties include both standard AND custom fields
const allProperties = schema.data.properties.map(p => p.name);
// ['firstname', 'lastname', 'email', 'custom_field_1', 'department', ...]

Handling Associations

HubSpot associations link objects together (e.g., Contact → Company, Deal → Contact).

Association Types:

  • HUBSPOT_DEFINED - Built-in associations (Contact to Company)
  • USER_DEFINED - Custom associations (Custom Object to Contact)

Key Pattern: Associations are discovered via schema introspection, then included in record fetch.

// 1. Get associations from schema
const schema = await getSchema(nango, objectId);
const associations = schema.associations
  .filter(a => a.fromObjectTypeId === objectId)
  .map(a => a.toObjectTypeId);

// 2. Fetch records with associations
const response = await nango.get({
  endpoint: `/crm/v3/objects/${objectType}`,
  params: {
    properties: properties.join(','),
    associations: associations.join(',') // Include in request!
  }
});

// 3. Response includes associations in each record
const record = response.data.results[0];
// record.associations.companies.results = [{ id: "123" }, ...]

Property Chunking Pattern

HubSpot has limits on URL length and number of properties per request. For objects with many custom fields:

Problem: 100+ properties can exceed URL limits

Solution: Chunk properties into groups, fetch multiple times, merge results

// Split properties into chunks of 50
const chunks = chunkArray(properties, 50);

const recordMap = new Map();

for (const propertyChunk of chunks) {
  const response = await nango.get({
    endpoint: `/crm/v3/objects/contacts`,
    params: {
      properties: propertyChunk.join(','),
      after: cursor
    }
  });

  // Merge properties from multiple requests
  for (const record of response.data.results) {
    const existing = recordMap.get(record.id);
    if (existing) {
      recordMap.set(record.id, {
        ...existing,
        properties: { ...existing.properties, ...record.properties }
      });
    } else {
      recordMap.set(record.id, record);
    }
  }
}

// All properties now merged in recordMap

Owner Fields Special Case

HubSpot has special "owner" fields that reference the Owner object:

  • hubspot_owner_id
  • hs_owner_id

These should be treated as lookups/foreign keys to the Owner object (owner).

// When mapping fields, detect owner fields
if (['hubspot_owner_id', 'hs_owner_id'].includes(property.name)) {
  // This is a reference to Owner object, not a simple field
  field.type = 'lookup';
  field.externalLinkTargetTable = 'owner';
}

HubSpot-Specific Implementation Examples

Full Sync: Use Standard CRM Endpoints

HubSpot Endpoint: /crm/v3/objects/{object} (GET with query params) Rate Limit: 190 calls per 10 seconds (up to 250 with capacity pack)

// HubSpot-specific considerations:
const properties = [
    'firstname',      // HubSpot uses lowercase, no camelCase
    'lastname',
    'email',
    'jobtitle',
    'createdate',     // Note: 'createdate' not 'createdDate'
    'hubspot_owner_id' // HubSpot prefix for system properties
];

const config: ProxyConfiguration = {
    endpoint: '/crm/v3/objects/contacts', // Standard CRM endpoint
    params: {
        properties: properties.join(',') // HubSpot requires comma-separated string
    },
    // ... pagination config
};

Incremental Sync: Must Use Search API

HubSpot Endpoint: /crm/v3/objects/{object}/search (POST with filter body) Rate Limit: 4 requests per second (much lower!) Why Required: Only endpoint supporting hs_lastmodifieddate filtering

// HubSpot-specific: Convert lastSyncDate to UNIX timestamp in milliseconds
const lastSyncDate = nango.lastSyncDate?.toISOString().slice(0, -8).replace('T', ' ');
const queryDate = lastSyncDate ? Date.parse(lastSyncDate) : Date.now() - 86400000;

const payload = {
    endpoint: '/crm/v3/objects/tickets/search', // POST, not GET
    data: {
        sorts: [{
            propertyName: 'hs_lastmodifieddate', // HubSpot-specific property
            direction: 'DESCENDING'
        }],
        properties: TICKET_PROPERTIES, // Array, not comma-separated string
        filterGroups: [{
            filters: [{
                propertyName: 'hs_lastmodifieddate', // Key for incremental
                operator: 'GT',                       // Greater than last sync
                value: queryDate                      // UNIX ms timestamp
            }]
        }],
        limit: `${MAX_PAGE}`,
        after: afterLink // Cursor for pagination
    },
    retries: 10
};

const response = await nango.post(payload);

HubSpot Pagination: Response includes paging.next.after cursor token.

Actions: Use Standard CRM Endpoints (Not Search)

HubSpot Endpoint: /crm/v3/objects/{object} (POST for create) Rate Limit: 190 calls per 10 seconds (higher than Search API)

// HubSpot requires properties wrapped in 'properties' object
const hubSpotContact = {
    properties: {
        firstname: input.firstName,
        lastname: input.lastName,
        email: input.email,
        jobtitle: input.jobTitle
    }
};

const config: ProxyConfiguration = {
    endpoint: 'crm/v3/objects/contacts', // Standard endpoint, NOT /search
    data: hubSpotContact,
    retries: 3
};

const response = await nango.post(config);
// HubSpot returns: { id, properties: {...}, createdAt, updatedAt, archived }

HubSpot-Specific Considerations

Object Type Variations

Different HubSpot objects may have different property names:

  • Always check HubSpot's API documentation for the specific object
  • Use the Search API with a test filter to verify property names
  • Common objects: contacts, companies, deals, tickets, products, line_items

Pagination Strategy

Search API Pagination:

  • Uses cursor-based pagination via after parameter
  • Returns paging.next.after for the next page
  • Limited to 10,000 total results

If you hit the 10,000 limit:

  • Add additional filters to narrow results
  • Consider splitting by date ranges
  • Process in smaller time windows

HubSpot Rate Limits

Standard Endpoints: 190 calls per 10 seconds (250 with capacity pack) Search API: 4 requests per second per token

Implication: Search API can make ~24 requests per 10 seconds vs 190 for standard endpoints.

HubSpot Error Codes

  • 400: Invalid property name or filter syntax
  • 429: Rate limit exceeded
  • 403: Missing OAuth scopes

HubSpot-Specific Checklist

When implementing HubSpot integrations, verify these HubSpot-specific requirements:

API Endpoint Selection

  • Using Search API (/crm/v3/objects/{object}/search) for incremental syncs ONLY
  • Using standard CRM endpoints (/crm/v3/objects/{object}) for full syncs and actions
  • Correct HTTP method: POST for Search API, GET for standard list endpoints

HubSpot Property Names

  • Correct property for last modified: lastmodifieddate (contacts) or hs_lastmodifieddate (other objects)
  • Using lowercase property names (firstname, not firstName)
  • Using hubspot_owner_id (with prefix) for owner references
  • Properties as comma-separated string for standard endpoints, array for Search API

HubSpot Data Formats

  • Timestamp in milliseconds (via Date.parse())
  • Request body wraps properties in properties object for create/update
  • Response includes properties object, not flat structure

HubSpot Rate Limits & Pagination

  • Aware of 4 req/sec limit for Search API (vs 190 per 10 sec for standard)
  • Pagination via paging.next.after cursor (not offset-based)
  • Search API limited to 10,000 results max

Schema Introspection & Custom Fields

  • Using schema introspection endpoints for flexible integrations
  • Fetching schemas first to discover custom fields dynamically
  • Caching schema information in metadata
  • Building property lists from schema (not hardcoding)
  • Property chunking for objects with many custom fields (50 properties per request)
  • Merging chunked responses by record ID
  • Handling associations discovered via schema
  • Special treatment of owner fields (hubspot_owner_id, hs_owner_id)
  • Distinguishing custom objects from standard objects via ID format
  • Using crm.schemas.custom.read scope for schema access

Common HubSpot-Specific Mistakes

API Endpoint & Protocol

  1. Using standard CRM endpoints for incremental syncs - They don't support hs_lastmodifieddate filtering; must use Search API
  2. Using Search API for actions - Use standard endpoints for better rate limits
  3. Wrong HTTP method - Search API is POST, not GET

Property & Field Handling

  1. Hardcoding property lists - Use schema introspection to discover custom fields dynamically
  2. Wrong property name for last modified - Using hs_lastmodifieddate for contacts (should be lastmodifieddate)
  3. CamelCase property names - HubSpot uses lowercase (firstname, not firstName)
  4. Missing properties wrapper - Create/update requests need { properties: {...} }
  5. Wrong properties format - Comma-separated string for GET, array for Search API POST
  6. Exceeding URL length limits - Chunk properties into groups of 50 for objects with many custom fields
  7. Not merging chunked responses - When fetching properties in chunks, merge by record ID

Data Format & Timestamps

  1. Date format errors - HubSpot requires UNIX timestamp in milliseconds, not seconds

Rate Limits & Performance

  1. Ignoring Search API rate limits - 4 req/sec is much lower than standard 190 per 10 sec
  2. Exceeding 10,000 result limit - Search API caps at 10k results per query

Schema & Custom Objects

  1. Not fetching schemas before records - Schema provides critical info about custom fields and associations
  2. Treating owner fields as simple properties - hubspot_owner_id should be treated as lookup to Owner object
  3. Not handling custom object IDs - Custom objects have different ID formats than standard objects

When to Use This Skill

Use this skill for HubSpot-specific questions:

  • Choosing between Search API and standard CRM endpoints
  • HubSpot property name variations (lastmodifieddate vs hs_lastmodifieddate)
  • HubSpot data format requirements (timestamps, property wrappers)
  • HubSpot rate limits and pagination quirks
  • Schema introspection for custom fields and associations
  • Two-phase sync pattern (schema → records)
  • Handling custom objects vs standard objects
  • Property chunking for objects with many fields
  • OAuth introspection endpoints
  • Troubleshooting HubSpot API errors

Do NOT use for generic Nango patterns - focus on HubSpot API specifics only.

HubSpot API Resources

  • Schema Introspection: https://developers.hubspot.com/docs/api/crm/properties
  • Object Schemas: https://developers.hubspot.com/docs/guides/api/crm/using-object-apis
  • Search API: https://developers.hubspot.com/docs/api/crm/search
  • OAuth: https://developers.hubspot.com/docs/api/working-with-oauth
  • Rate Limits: https://developers.hubspot.com/docs/api/usage-details
  • CRM Objects: https://developers.hubspot.com/docs/api/crm/understanding-the-crm
  • Scopes: https://developers.hubspot.com/docs/apps/legacy-apps/authentication/scopes