Claude-code-plugins-plus-skills hubspot-known-pitfalls
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/hubspot-pack/skills/hubspot-known-pitfalls" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-hubspot-known-pitfalls && rm -rf "$T"
manifest:
plugins/saas-packs/hubspot-pack/skills/hubspot-known-pitfalls/SKILL.mdsource content
HubSpot Known Pitfalls
Overview
Ten real-world HubSpot API anti-patterns with correct alternatives, covering authentication, rate limits, search, associations, and data handling.
Prerequisites
- Access to HubSpot integration codebase
- Understanding of HubSpot CRM v3 API
Instructions
Pitfall 1: Using Deprecated API Keys
// BAD: API keys were deprecated in 2022 and removed from SDK v10+ const client = new hubspot.Client({ apiKey: 'your-api-key' }); // REMOVED // GOOD: Use private app access token const client = new hubspot.Client({ accessToken: process.env.HUBSPOT_ACCESS_TOKEN!, // pat-na1-xxxxx numberOfApiCallRetries: 3, });
Pitfall 2: Not Using Batch Operations
// BAD: N API calls to read N contacts (hits rate limit fast) for (const id of contactIds) { const contact = await client.crm.contacts.basicApi.getById(id, ['email']); // 100 contacts = 100 API calls } // GOOD: 1 API call for up to 100 contacts const batch = await client.crm.contacts.batchApi.read({ inputs: contactIds.map(id => ({ id })), properties: ['email', 'firstname'], propertiesWithHistory: [], }); // 100 contacts = 1 API call
Pitfall 3: Ignoring Search Limits
// BAD: Search API has a hard limit of 10,000 results total // You cannot page past this limit with `after` const allResults = []; let after = 0; do { const page = await client.crm.contacts.searchApi.doSearch({ filterGroups: [], properties: ['email'], limit: 100, after, sorts: [], }); allResults.push(...page.results); after = page.paging?.next?.after; } while (after); // STOPS at 10,000 regardless // GOOD: Use getPage for full exports (no 10K limit) async function* getAllContacts(properties: string[]) { let after: string | undefined; do { const page = await client.crm.contacts.basicApi.getPage(100, after, properties); yield* page.results; after = page.paging?.next?.after; } while (after); // No upper limit }
Pitfall 4: Wrong Association Type IDs
// BAD: Guessing association type IDs await client.crm.associations.v4.basicApi.create( 'contacts', contactId, 'companies', companyId, [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 999 }] // wrong ID! ); // Error: "association type id 999 doesn't exist between contact and company" // GOOD: Use documented default type IDs const ASSOC_TYPES = { CONTACT_TO_COMPANY: 1, // Primary company CONTACT_TO_DEAL: 3, COMPANY_TO_DEAL: 5, CONTACT_TO_TICKET: 16, NOTE_TO_CONTACT: 202, TASK_TO_CONTACT: 204, NOTE_TO_DEAL: 214, }; await client.crm.associations.v4.basicApi.create( 'contacts', contactId, 'companies', companyId, [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: ASSOC_TYPES.CONTACT_TO_COMPANY }] ); // Or look up dynamically: // GET /crm/v4/associations/contacts/companies/labels
Pitfall 5: Creating Duplicate Contacts
// BAD: Create without checking if contact exists await client.crm.contacts.basicApi.create({ properties: { email: 'jane@example.com', firstname: 'Jane' }, associations: [], }); // If jane@example.com exists: 409 Conflict error // GOOD: Search first, then create or update async function upsertContact(email: string, props: Record<string, string>) { const existing = await client.crm.contacts.searchApi.doSearch({ filterGroups: [{ filters: [{ propertyName: 'email', operator: 'EQ', value: email }], }], properties: ['email'], limit: 1, after: 0, sorts: [], }); if (existing.results.length > 0) { return client.crm.contacts.basicApi.update(existing.results[0].id, { properties: props }); } return client.crm.contacts.basicApi.create({ properties: { email, ...props }, associations: [], }); } // BETTER: Use batch upsert (single API call) await client.apiRequest({ method: 'POST', path: '/crm/v3/objects/contacts/batch/upsert', body: { inputs: [{ properties: { email: 'jane@example.com', firstname: 'Jane' }, idProperty: 'email', id: 'jane@example.com' }], }, });
Pitfall 6: Requesting All Properties
// BAD: No properties specified = returns ALL default properties const contact = await client.crm.contacts.basicApi.getById('123'); // Returns ~50+ properties you don't need, slower response // GOOD: Request only what you need const contact = await client.crm.contacts.basicApi.getById('123', [ 'email', 'firstname', 'lastname', 'lifecyclestage', ]); // Returns only 4 properties, faster response
Pitfall 7: Hardcoding Pipeline Stage IDs
// BAD: Stage IDs are portal-specific, not universal const deal = await client.crm.deals.basicApi.create({ properties: { dealname: 'New Deal', dealstage: 'appointmentscheduled', // this default ID might not exist pipeline: 'default', // might not be called "default" }, associations: [], }); // GOOD: Fetch pipelines first, then use IDs const pipelines = await client.crm.pipelines.pipelinesApi.getAll('deals'); const salesPipeline = pipelines.results[0]; // or find by label const firstStage = salesPipeline.stages.sort( (a, b) => Number(a.displayOrder) - Number(b.displayOrder) )[0]; const deal = await client.crm.deals.basicApi.create({ properties: { dealname: 'New Deal', dealstage: firstStage.id, pipeline: salesPipeline.id, }, associations: [], });
Pitfall 8: Not Handling 409 Conflict on Associations
// BAD: Creating association without checking if it exists // (Some association types allow only one, e.g., primary company) try { await client.crm.associations.v4.basicApi.create( 'contacts', contactId, 'companies', companyId, [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 1 }] ); } catch (error) { // 409: Association already exists -- this is actually OK! // Don't treat as error } // GOOD: Catch 409 gracefully async function ensureAssociation( fromType: string, fromId: string, toType: string, toId: string, typeId: number ) { try { await client.crm.associations.v4.basicApi.create( fromType, fromId, toType, toId, [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: typeId }] ); } catch (error: any) { if (error?.code !== 409) throw error; // Association already exists -- idempotent success } }
Pitfall 9: Polling Instead of Using Webhooks
// BAD: Polling for changes wastes API calls setInterval(async () => { const updated = await client.crm.contacts.searchApi.doSearch({ filterGroups: [{ filters: [{ propertyName: 'lastmodifieddate', operator: 'GTE', value: String(Date.now() - 60000), }], }], properties: ['email'], limit: 100, after: 0, sorts: [], }); processChanges(updated.results); }, 60000); // 1,440 API calls/day just for polling // GOOD: Use webhooks (0 API calls for change detection) // Set up webhook subscription in your HubSpot public app for: // - contact.propertyChange // - deal.propertyChange // - contact.creation // See hubspot-webhooks-events skill
Pitfall 10: Using the Wrong API Endpoint Version
// BAD: Using legacy v1/v2 endpoints const response = await fetch( `https://api.hubapi.com/contacts/v1/contact/email/${email}/profile`, { headers: { Authorization: `Bearer ${token}` } } ); // Legacy endpoints may be deprecated and have different auth requirements // GOOD: Use CRM v3 API with the SDK const result = await client.crm.contacts.searchApi.doSearch({ filterGroups: [{ filters: [{ propertyName: 'email', operator: 'EQ', value: email }], }], properties: ['firstname', 'lastname', 'email'], limit: 1, after: 0, sorts: [], });
Quick Scan Commands
# Detect these pitfalls in your codebase grep -rn "apiKey:" src/ --include="*.ts" # Pitfall 1 grep -rn "basicApi.getById" src/ | wc -l # Pitfall 2 (if > 10, use batch) grep -rn "contacts/v1\|deals/v1\|companies/v2" src/ # Pitfall 10 grep -rn "setInterval.*hubspot\|setInterval.*crm" src/ # Pitfall 9 grep -rn "pat-na1-" src/ --include="*.ts" --include="*.js" # Token leak
Quick Reference Card
| Pitfall | Detection | Fix |
|---|---|---|
| Deprecated API keys | | Use |
| No batching | Many calls | Use |
| Search > 10K | Search with past 10K | Use |
| Wrong association IDs | 400 errors on associate | Use documented type IDs |
| Duplicate contacts | 409 on create | Search first or batch upsert |
| All properties | No param | Specify needed fields |
| Hardcoded stage IDs | Stage not found errors | Fetch pipelines dynamically |
| Association conflict | 409 on associate | Catch and ignore 409 |
| Polling for changes | High API call volume | Use webhooks |
| Legacy API versions | or URLs | Use CRM v3 SDK |