--- name: hubspot-integration description: Expert patterns for HubSpot CRM integration including OAuth authentication, CRM objects, associations, batch operations, webhooks, and custom objects. Covers Node.js and Python SDKs. risk: unknown source: vibeship-spawner-skills (Apache 2.0) date_added: 2026-02-27 --- # HubSpot Integration Expert patterns for HubSpot CRM integration including OAuth authentication, CRM objects, associations, batch operations, webhooks, and custom objects. Covers Node.js and Python SDKs. ## Patterns ### OAuth 2.0 Authentication Secure authentication for public apps **When to use**: Building public app or multi-account integration ### Template // OAuth 2.0 flow for HubSpot import { Client } from "@hubspot/api-client"; // Environment variables const CLIENT_ID = process.env.HUBSPOT_CLIENT_ID; const CLIENT_SECRET = process.env.HUBSPOT_CLIENT_SECRET; const REDIRECT_URI = process.env.HUBSPOT_REDIRECT_URI; const SCOPES = "crm.objects.contacts.read crm.objects.contacts.write"; // Step 1: Generate authorization URL function getAuthUrl(): string { const authUrl = new URL("https://app.hubspot.com/oauth/authorize"); authUrl.searchParams.set("client_id", CLIENT_ID); authUrl.searchParams.set("redirect_uri", REDIRECT_URI); authUrl.searchParams.set("scope", SCOPES); return authUrl.toString(); } // Step 2: Handle OAuth callback async function handleOAuthCallback(code: string) { const response = await fetch("https://api.hubapi.com/oauth/v1/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "authorization_code", client_id: CLIENT_ID, client_secret: CLIENT_SECRET, redirect_uri: REDIRECT_URI, code: code, }), }); const tokens = await response.json(); // { // access_token: "xxx", // refresh_token: "xxx", // expires_in: 1800 // 30 minutes // } // Store tokens securely await storeTokens(tokens); return tokens; } // Step 3: Refresh access token (before expiry) async function refreshAccessToken(refreshToken: string) { const response = await fetch("https://api.hubapi.com/oauth/v1/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", client_id: CLIENT_ID, client_secret: CLIENT_SECRET, refresh_token: refreshToken, }), }); return response.json(); } // Step 4: Create authenticated client function createClient(accessToken: string): Client { const hubspotClient = new Client({ accessToken }); return hubspotClient; } ### Notes - Access tokens expire in 30 minutes - Refresh tokens before expiry - Store refresh tokens securely - Rotate tokens every 6 months ### Private App Token Authentication for single-account integrations **When to use**: Building internal integration for one HubSpot account ### Template // Private App Token - simpler for single account import { Client } from "@hubspot/api-client"; // Create client with private app token const hubspotClient = new Client({ accessToken: process.env.HUBSPOT_PRIVATE_APP_TOKEN, }); // Private app tokens don't expire // But should be rotated every 6 months for security // Example: Get contacts async function getContacts() { try { const response = await hubspotClient.crm.contacts.basicApi.getPage( 100, // limit undefined, // after cursor ["firstname", "lastname", "email", "phone"], // properties ); return response.results; } catch (error) { if (error.code === 429) { // Rate limited - implement backoff const retryAfter = error.headers?.["retry-after"] || 10; await sleep(retryAfter * 1000); return getContacts(); } throw error; } } // Python equivalent // from hubspot import HubSpot // // client = HubSpot(access_token=os.environ["HUBSPOT_PRIVATE_APP_TOKEN"]) // // contacts = client.crm.contacts.basic_api.get_page( // limit=100, // properties=["firstname", "lastname", "email"] // ) ### Notes - Private app tokens don't expire - All private apps share daily rate limit - Each private app has own burst limit - Recommended: Rotate every 6 months ### CRM Object CRUD Operations Create, read, update, delete CRM records **When to use**: Working with contacts, companies, deals, tickets ### Template import { Client } from "@hubspot/api-client"; const hubspotClient = new Client({ accessToken: process.env.HUBSPOT_TOKEN, }); // CREATE contact async function createContact(data: { email: string; firstname: string; lastname: string; }) { const response = await hubspotClient.crm.contacts.basicApi.create({ properties: { email: data.email, firstname: data.firstname, lastname: data.lastname, }, }); return response; } // READ contact by ID async function getContact(contactId: string) { const response = await hubspotClient.crm.contacts.basicApi.getById( contactId, ["firstname", "lastname", "email", "phone", "company"], ); return response; } // UPDATE contact async function updateContact(contactId: string, properties: object) { const response = await hubspotClient.crm.contacts.basicApi.update( contactId, { properties }, ); return response; } // DELETE contact async function deleteContact(contactId: string) { await hubspotClient.crm.contacts.basicApi.archive(contactId); } // SEARCH contacts async function searchContacts(query: string) { const response = await hubspotClient.crm.contacts.searchApi.doSearch({ query, limit: 100, properties: ["firstname", "lastname", "email"], sorts: [{ propertyName: "createdate", direction: "DESCENDING" }], }); return response.results; } // LIST with pagination async function getAllContacts() { const allContacts = []; let after = undefined; do { const response = await hubspotClient.crm.contacts.basicApi.getPage( 100, after, ["firstname", "lastname", "email"], ); allContacts.push(...response.results); after = response.paging?.next?.after; } while (after); return allContacts; } ### Notes - Use properties param to fetch only needed fields - Search API has 10k result limit - Always implement pagination for lists - Archive (soft delete) vs. GDPR delete available ### Batch Operations Bulk create, update, or read records efficiently **When to use**: Processing multiple records (reduce rate limit usage) ### Template import { Client } from "@hubspot/api-client"; const hubspotClient = new Client({ accessToken: process.env.HUBSPOT_TOKEN, }); // BATCH CREATE contacts (up to 100 per batch) async function batchCreateContacts(contacts: Array<{ email: string; firstname: string; lastname: string; }>) { const inputs = contacts.map((contact) => ({ properties: { email: contact.email, firstname: contact.firstname, lastname: contact.lastname, }, })); const response = await hubspotClient.crm.contacts.batchApi.create({ inputs, }); return response.results; } // BATCH UPDATE contacts async function batchUpdateContacts( updates: Array<{ id: string; properties: object }> ) { const inputs = updates.map(({ id, properties }) => ({ id, properties, })); const response = await hubspotClient.crm.contacts.batchApi.update({ inputs, }); return response.results; } // BATCH READ contacts by ID async function batchReadContacts( ids: string[], properties: string[] = ["firstname", "lastname", "email"] ) { const response = await hubspotClient.crm.contacts.batchApi.read({ inputs: ids.map((id) => ({ id })), properties, }); return response.results; } // BATCH ARCHIVE contacts async function batchDeleteContacts(ids: string[]) { await hubspotClient.crm.contacts.batchApi.archive({ inputs: ids.map((id) => ({ id })), }); } // Process large dataset in chunks async function processLargeDataset(allContacts: any[]) { const BATCH_SIZE = 100; const results = []; for (let i = 0; i < allContacts.length; i += BATCH_SIZE) { const batch = allContacts.slice(i, i + BATCH_SIZE); const batchResults = await batchCreateContacts(batch); results.push(...batchResults); // Respect rate limits - wait between batches if (i + BATCH_SIZE < allContacts.length) { await sleep(100); // 100ms between batches } } return results; } ### Notes - Max 100 items per batch request - Saves up to 80% of rate limit quota - Batch operations are atomic per item (partial success possible) - Check response.errors for failed items ### Associations v4 API Create relationships between CRM records **When to use**: Linking contacts to companies, deals, etc. ### Template import { Client, AssociationTypes } from "@hubspot/api-client"; const hubspotClient = new Client({ accessToken: process.env.HUBSPOT_TOKEN, }); // CREATE association (Contact to Company) async function associateContactToCompany( contactId: string, companyId: string ) { await hubspotClient.crm.associations.v4.basicApi.create( "contacts", contactId, "companies", companyId, [ { associationCategory: "HUBSPOT_DEFINED", associationTypeId: AssociationTypes.contactToCompany, }, ] ); } // CREATE association (Deal to Contact) async function associateDealToContact(dealId: string, contactId: string) { await hubspotClient.crm.associations.v4.basicApi.create( "deals", dealId, "contacts", contactId, [ { associationCategory: "HUBSPOT_DEFINED", associationTypeId: 3, // deal_to_contact }, ] ); } // GET associations for a record async function getContactCompanies(contactId: string) { const response = await hubspotClient.crm.associations.v4.basicApi.getPage( "contacts", contactId, "companies", undefined, 500 ); return response.results; } // CREATE association with custom label async function createLabeledAssociation( contactId: string, companyId: string, labelId: number // Custom association label ID ) { await hubspotClient.crm.associations.v4.basicApi.create( "contacts", contactId, "companies", companyId, [ { associationCategory: "USER_DEFINED", associationTypeId: labelId, }, ] ); } // BATCH create associations async function batchAssociateContactsToCompany( contactIds: string[], companyId: string ) { const inputs = contactIds.map((contactId) => ({ _from: { id: contactId }, to: { id: companyId }, types: [ { associationCategory: "HUBSPOT_DEFINED", associationTypeId: AssociationTypes.contactToCompany, }, ], })); await hubspotClient.crm.associations.v4.batchApi.create( "contacts", "companies", { inputs } ); } // Common association type IDs // Contact to Company: 1 // Company to Contact: 2 // Deal to Contact: 3 // Contact to Deal: 4 // Deal to Company: 5 // Company to Deal: 6 ### Notes - Requires SDK version 9.0.0+ for v4 API - Association labels supported for custom relationships - Use batch API for multiple associations - HUBSPOT_DEFINED for standard, USER_DEFINED for custom labels ### Webhook Handling Receive real-time notifications from HubSpot **When to use**: Need instant updates on CRM changes ### Template import crypto from "crypto"; import { Client } from "@hubspot/api-client"; // Webhook signature validation function validateWebhookSignature( requestBody: string, signature: string, clientSecret: string ): boolean { // For v2 signature (most common) const expectedSignature = crypto .createHmac("sha256", clientSecret) .update(requestBody) .digest("hex"); return signature === expectedSignature; } // Express webhook handler app.post("/webhooks/hubspot", async (req, res) => { const signature = req.headers["x-hubspot-signature-v3"] as string; const timestamp = req.headers["x-hubspot-request-timestamp"] as string; const requestBody = JSON.stringify(req.body); // Validate signature const isValid = validateWebhookSignature( requestBody, signature, process.env.HUBSPOT_CLIENT_SECRET ); if (!isValid) { console.error("Invalid webhook signature"); return res.status(401).send("Unauthorized"); } // Check timestamp (prevent replay attacks) const timestampAge = Date.now() - parseInt(timestamp); if (timestampAge > 300000) { // 5 minutes console.error("Webhook timestamp too old"); return res.status(401).send("Timestamp expired"); } // Process events - respond quickly! const events = req.body; // Queue for async processing for (const event of events) { await queue.add("hubspot-webhook", event); } // Respond immediately res.status(200).send("OK"); }); // Async processor async function processWebhookEvent(event: any) { const { subscriptionType, objectId, propertyName, propertyValue } = event; switch (subscriptionType) { case "contact.creation": await handleContactCreated(objectId); break; case "contact.propertyChange": await handleContactPropertyChange(objectId, propertyName, propertyValue); break; case "deal.creation": await handleDealCreated(objectId); break; case "contact.deletion": await handleContactDeleted(objectId); break; default: console.log(`Unhandled event: ${subscriptionType}`); } } // Webhook subscription types: // contact.creation, contact.deletion, contact.propertyChange // company.creation, company.deletion, company.propertyChange // deal.creation, deal.deletion, deal.propertyChange ### Notes - Validate signature before processing - Respond within 5 seconds - Queue heavy processing for async - Max 1000 webhook subscriptions per app ### Custom Objects Create and manage custom object types **When to use**: Standard objects don't fit your data model ### Template import { Client } from "@hubspot/api-client"; const hubspotClient = new Client({ accessToken: process.env.HUBSPOT_TOKEN, }); // CREATE custom object schema async function createCustomObjectSchema() { const schema = { name: "projects", labels: { singular: "Project", plural: "Projects", }, primaryDisplayProperty: "project_name", requiredProperties: ["project_name"], properties: [ { name: "project_name", label: "Project Name", type: "string", fieldType: "text", }, { name: "status", label: "Status", type: "enumeration", fieldType: "select", options: [ { label: "Active", value: "active" }, { label: "Completed", value: "completed" }, { label: "On Hold", value: "on_hold" }, ], }, { name: "budget", label: "Budget", type: "number", fieldType: "number", }, { name: "start_date", label: "Start Date", type: "date", fieldType: "date", }, ], associatedObjects: ["CONTACT", "COMPANY"], }; const response = await hubspotClient.crm.schemas.coreApi.create(schema); return response; } // CREATE custom object record async function createProject(data: { project_name: string; status: string; budget: number; }) { const response = await hubspotClient.crm.objects.basicApi.create( "projects", // Custom object name { properties: data } ); return response; } // READ custom object by ID async function getProject(projectId: string) { const response = await hubspotClient.crm.objects.basicApi.getById( "projects", projectId, ["project_name", "status", "budget", "start_date"] ); return response; } // UPDATE custom object async function updateProject(projectId: string, properties: object) { const response = await hubspotClient.crm.objects.basicApi.update( "projects", projectId, { properties } ); return response; } // SEARCH custom objects async function searchProjects(status: string) { const response = await hubspotClient.crm.objects.searchApi.doSearch( "projects", { filterGroups: [ { filters: [ { propertyName: "status", operator: "EQ", value: status, }, ], }, ], properties: ["project_name", "status", "budget"], limit: 100, } ); return response.results; } ### Notes - Custom objects require Enterprise tier - Max 10 custom objects per account - Use crm.objects API with object name as parameter - Can associate with standard and other custom objects ## Sharp Edges ### Rate Limits Vary by App Type and Hub Tier Severity: HIGH ### 5% Error Rate Threshold for Marketplace Apps Severity: HIGH ### API Keys Deprecated - Use OAuth or Private App Tokens Severity: CRITICAL ### OAuth Access Tokens Expire in 30 Minutes Severity: HIGH ### Webhook Requests Must Be Validated Severity: CRITICAL ### All List Endpoints Require Pagination Severity: MEDIUM ### Associations v4 API Has Breaking Changes Severity: HIGH ### Polling Limited to 100,000 Requests Per Day Severity: MEDIUM ## Validation Checks ### Hardcoded HubSpot API Key Severity: ERROR API keys must never be hardcoded Message: Hardcoded HubSpot API key detected. Use environment variables. Note: API keys are deprecated - use Private App tokens. ### Hardcoded HubSpot Access Token Severity: ERROR Access tokens must use environment variables Message: Hardcoded HubSpot access token. Use environment variables. ### Hardcoded Client Secret Severity: ERROR OAuth client secrets must be secured Message: Hardcoded client secret. Use environment variables. ### Missing Webhook Signature Validation Severity: ERROR Webhook endpoints must validate HubSpot signatures Message: Webhook endpoint without signature validation. Validate X-HubSpot-Signature-v3. ### Missing Rate Limit Handling Severity: WARNING API calls should handle 429 responses Message: HubSpot API calls without rate limit handling. Implement retry logic with backoff. ### Unthrottled Parallel API Calls Severity: WARNING Parallel calls can exceed rate limits Message: Parallel HubSpot API calls without throttling. Use rate limiter. ### Missing Pagination for List Calls Severity: WARNING List endpoints return paginated results Message: API call without pagination handling. Implement cursor-based pagination. ### Individual Operations in Loop Severity: INFO Use batch operations for multiple items Message: Individual API calls in loop. Consider batch operations for better performance. ### Token Storage Without Expiry Severity: WARNING OAuth tokens expire and need refresh logic Message: Token storage without expiry tracking. Store expiresAt for refresh logic. ### Deprecated API Key Usage Severity: ERROR API keys are deprecated Message: Using deprecated API key. Migrate to Private App token or OAuth 2.0. ## Collaboration ### Delegation Triggers - user needs email marketing automation -> email-marketing (Beyond HubSpot's built-in email tools) - user needs custom CRM UI -> frontend (Building portal or dashboard) - user needs data pipeline -> data-engineer (ETL from HubSpot to warehouse) - user needs Salesforce integration -> salesforce-development (HubSpot + Salesforce sync) - user needs payment processing -> stripe-integration (Payments beyond HubSpot quotes) - user needs analytics dashboard -> analytics-specialist (Custom reporting beyond HubSpot) ## When to Use - User mentions or implies: hubspot - User mentions or implies: hubspot api - User mentions or implies: hubspot crm - User mentions or implies: hubspot integration - User mentions or implies: contacts api ## Limitations - Use this skill only when the task clearly matches the scope described above. - Do not treat the output as a substitute for environment-specific validation, testing, or expert review. - Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.