Agent Skills: HubSpot SDK Patterns

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/hubspot-sdk-patterns

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/hubspot-pack/skills/hubspot-sdk-patterns

Skill Files

Browse the full folder contents for hubspot-sdk-patterns.

Download Skill

Loading file tree…

plugins/saas-packs/hubspot-pack/skills/hubspot-sdk-patterns/SKILL.md

Skill Metadata

Name
hubspot-sdk-patterns
Description
|

HubSpot SDK Patterns

Overview

Production-ready patterns for the @hubspot/api-client SDK covering typed wrappers, error handling, batch operations, and pagination.

Prerequisites

  • @hubspot/api-client v13+ installed
  • TypeScript 5+ with strict mode
  • Understanding of HubSpot CRM object model

Instructions

Step 1: Typed Client Wrapper

// src/hubspot/client.ts
import * as hubspot from '@hubspot/api-client';
import type {
  SimplePublicObjectInputForCreate,
  SimplePublicObject,
  PublicObjectSearchRequest,
} from '@hubspot/api-client/lib/codegen/crm/contacts';

interface HubSpotConfig {
  accessToken: string;
  retries?: number;
}

let instance: hubspot.Client | null = null;

export function getClient(config?: HubSpotConfig): hubspot.Client {
  if (!instance) {
    instance = new hubspot.Client({
      accessToken: config?.accessToken || process.env.HUBSPOT_ACCESS_TOKEN!,
      numberOfApiCallRetries: config?.retries ?? 3,
    });
  }
  return instance;
}

Step 2: Error Classification

// src/hubspot/errors.ts
export class HubSpotApiError extends Error {
  constructor(
    message: string,
    public readonly statusCode: number,
    public readonly category: string,
    public readonly correlationId: string,
    public readonly retryable: boolean
  ) {
    super(message);
    this.name = 'HubSpotApiError';
  }
}

export function classifyError(error: any): HubSpotApiError {
  const status = error?.code || error?.statusCode || error?.response?.status || 500;
  const body = error?.body || error?.response?.body || {};
  const correlationId = body.correlationId || 'unknown';

  const retryable = [429, 500, 502, 503, 504].includes(status);

  const categoryMap: Record<number, string> = {
    400: 'VALIDATION_ERROR',
    401: 'AUTHENTICATION_ERROR',
    403: 'AUTHORIZATION_ERROR',
    404: 'NOT_FOUND',
    409: 'CONFLICT',
    429: 'RATE_LIMIT',
    500: 'INTERNAL_ERROR',
  };

  return new HubSpotApiError(
    body.message || error.message || 'Unknown HubSpot error',
    status,
    categoryMap[status] || 'UNKNOWN',
    correlationId,
    retryable
  );
}

// Usage wrapper
export async function safeCall<T>(operation: () => Promise<T>): Promise<T> {
  try {
    return await operation();
  } catch (error) {
    throw classifyError(error);
  }
}

Step 3: Typed CRM Operations

// src/hubspot/contacts.ts
import * as hubspot from '@hubspot/api-client';
import type { SimplePublicObject } from '@hubspot/api-client/lib/codegen/crm/contacts';
import { getClient } from './client';
import { safeCall } from './errors';

// Define your contact properties as a type
interface ContactProperties {
  firstname?: string;
  lastname?: string;
  email: string;
  phone?: string;
  company?: string;
  lifecyclestage?: 'subscriber' | 'lead' | 'marketingqualifiedlead'
    | 'salesqualifiedlead' | 'opportunity' | 'customer' | 'evangelist';
}

const CONTACT_PROPS = ['firstname', 'lastname', 'email', 'phone', 'company', 'lifecyclestage'];

export async function createContact(props: ContactProperties): Promise<SimplePublicObject> {
  return safeCall(() =>
    getClient().crm.contacts.basicApi.create({
      properties: props as Record<string, string>,
      associations: [],
    })
  );
}

export async function getContact(id: string): Promise<SimplePublicObject> {
  return safeCall(() =>
    getClient().crm.contacts.basicApi.getById(id, CONTACT_PROPS)
  );
}

export async function updateContact(
  id: string,
  props: Partial<ContactProperties>
): Promise<SimplePublicObject> {
  return safeCall(() =>
    getClient().crm.contacts.basicApi.update(id, {
      properties: props as Record<string, string>,
    })
  );
}

export async function findContactByEmail(email: string): Promise<SimplePublicObject | null> {
  const result = await safeCall(() =>
    getClient().crm.contacts.searchApi.doSearch({
      filterGroups: [{
        filters: [{ propertyName: 'email', operator: 'EQ', value: email }],
      }],
      properties: CONTACT_PROPS,
      limit: 1,
      after: 0,
      sorts: [],
    })
  );
  return result.results[0] || null;
}

Step 4: Batch Operations

// src/hubspot/batch.ts
import { getClient } from './client';
import { safeCall } from './errors';

// Batch create contacts (max 100 per batch)
export async function batchCreateContacts(
  contacts: Array<{ properties: Record<string, string> }>
) {
  // POST /crm/v3/objects/contacts/batch/create
  return safeCall(() =>
    getClient().crm.contacts.batchApi.create({
      inputs: contacts.map(c => ({ properties: c.properties, associations: [] })),
    })
  );
}

// Batch read contacts by ID or unique property
export async function batchReadContacts(ids: string[], properties: string[]) {
  // POST /crm/v3/objects/contacts/batch/read
  return safeCall(() =>
    getClient().crm.contacts.batchApi.read({
      inputs: ids.map(id => ({ id })),
      properties,
      propertiesWithHistory: [],
    })
  );
}

// Batch upsert: create or update in one call
export async function batchUpsertContacts(
  contacts: Array<{ properties: Record<string, string> }>,
  idProperty = 'email'
) {
  // POST /crm/v3/objects/contacts/batch/upsert
  const client = getClient();
  return safeCall(() =>
    client.apiRequest({
      method: 'POST',
      path: '/crm/v3/objects/contacts/batch/upsert',
      body: {
        inputs: contacts.map(c => ({
          properties: c.properties,
          idProperty,
          id: c.properties[idProperty],
        })),
      },
    })
  );
}

// Chunk large batches into 100-record groups
export async function batchCreateChunked(
  contacts: Array<{ properties: Record<string, string> }>,
  chunkSize = 100
) {
  const results = [];
  for (let i = 0; i < contacts.length; i += chunkSize) {
    const chunk = contacts.slice(i, i + chunkSize);
    const result = await batchCreateContacts(chunk);
    results.push(result);
  }
  return results;
}

Step 5: Pagination Helper

// src/hubspot/pagination.ts
import type { SimplePublicObject } from '@hubspot/api-client/lib/codegen/crm/contacts';
import { getClient } from './client';

export async function* getAllContacts(
  properties: string[],
  limit = 100
): AsyncGenerator<SimplePublicObject> {
  let after: string | undefined;

  do {
    const page = await getClient().crm.contacts.basicApi.getPage(
      limit,
      after,
      properties
    );

    for (const contact of page.results) {
      yield contact;
    }

    after = page.paging?.next?.after;
  } while (after);
}

// Usage
async function exportAllContacts() {
  const allContacts: SimplePublicObject[] = [];
  for await (const contact of getAllContacts(['firstname', 'lastname', 'email'])) {
    allContacts.push(contact);
  }
  console.log(`Exported ${allContacts.length} contacts`);
}

Output

  • Type-safe client singleton with automatic retries
  • Error classification with retryable detection
  • Typed CRUD operations for contacts
  • Batch create/read/upsert with chunking
  • Async generator for paginated results

Error Handling

| Pattern | Use Case | Benefit | |---------|----------|---------| | safeCall wrapper | All API calls | Classifies errors, adds correlation IDs | | numberOfApiCallRetries | Transient failures | Built-in SDK retry for 429/5xx | | Batch chunking | Large datasets | Stays within 100-record batch limit | | Pagination generator | Full exports | Memory-efficient streaming |

Examples

Factory Pattern (Multi-Portal)

const clients = new Map<string, hubspot.Client>();

export function getClientForPortal(portalId: string, token: string): hubspot.Client {
  if (!clients.has(portalId)) {
    clients.set(portalId, new hubspot.Client({ accessToken: token }));
  }
  return clients.get(portalId)!;
}

Resources

Next Steps

Apply patterns in hubspot-core-workflow-a for real-world CRM operations.