From hubspot-pack
Applies @hubspot/api-client SDK patterns in TypeScript for HubSpot CRM integrations: typed wrappers, error handling, batching, and pagination.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin hubspot-packThis skill is limited to using the following tools:
Production-ready patterns for the `@hubspot/api-client` SDK covering typed wrappers, error handling, batch operations, and pagination.
Provides expert patterns for HubSpot CRM integration including OAuth authentication, CRM objects, associations, batch operations, webhooks, and custom objects using Node.js and Python SDKs.
Provides expert patterns for HubSpot CRM integration including OAuth authentication, CRM objects, associations, batch operations, webhooks, and custom objects using Node.js/Python SDKs.
Identifies HubSpot API anti-patterns like deprecated auth, missing batch ops, search limits, and wrong associations, with TypeScript fixes for code reviews and audits.
Share bugs, ideas, or general feedback.
Production-ready patterns for the @hubspot/api-client SDK covering typed wrappers, error handling, batch operations, and pagination.
@hubspot/api-client v13+ installed// 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;
}
// 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);
}
}
// 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;
}
// 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;
}
// 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`);
}
| 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 |
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)!;
}
Apply patterns in hubspot-core-workflow-a for real-world CRM operations.