Vibeship-spawner-skills hubspot-integration

HubSpot Integration Skill

install
source · Clone the upstream repo
git clone https://github.com/vibeforge1111/vibeship-spawner-skills
manifest: integrations/hubspot-integration/skill.yaml
source content

HubSpot Integration Skill

Expert patterns for HubSpot CRM API development

version: 1.0.0 name: HubSpot Integration id: hubspot-integration category: integrations tags: [hubspot, crm, marketing, sales, api]

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.

triggers:

  • "hubspot"
  • "hubspot api"
  • "hubspot crm"
  • "hubspot integration"
  • "contacts api"

anti_patterns:

  • name: Using Deprecated API Keys description: API keys deprecated in 2022 instead: Use OAuth 2.0 or Private App tokens

  • name: Individual Requests Instead of Batch description: Single item updates waste rate limit quota instead: Use batch endpoints for multiple items

  • name: Polling Instead of Webhooks description: Wastes daily request quota (100k/day limit) instead: Use webhooks for real-time notifications

  • name: Ignoring Pagination description: Only processes first page of results instead: Always implement pagination with after cursor

  • name: Not Validating Webhook Signatures description: Security risk from spoofed webhooks instead: Validate X-HubSpot-Signature header

handoffs:

  • situation: User needs Salesforce instead delegate_to: salesforce-development context: Salesforce CRM integration

  • situation: User needs email marketing beyond HubSpot delegate_to: email-marketing context: SendGrid, Mailgun alternatives

  • situation: User needs workflow automation delegate_to: workflow-automation context: HubSpot workflows or external tools

patterns:

  • name: OAuth 2.0 Authentication description: Secure authentication for public apps when: 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"
  • name: Private App Token description: Authentication for single-account integrations when: 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"
  • name: CRM Object CRUD Operations description: Create, read, update, delete CRM records when: 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"
  • name: Batch Operations description: Bulk create, update, or read records efficiently when: 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"
  • name: Associations v4 API description: Create relationships between CRM records when: 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"
  • name: Webhook Handling description: Receive real-time notifications from HubSpot when: 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"
  • name: Custom Objects description: Create and manage custom object types when: 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"

best_practices:

  • practice: Use batch endpoints why: Reduces rate limit consumption by up to 80% implementation: | // Use batchApi instead of individual calls const results = await hubspotClient.crm.contacts.batchApi.update({ inputs: updates.map(u => ({ id: u.id, properties: u.props })) });

  • practice: Implement proper rate limit handling why: Prevents 429 errors and app failures implementation: | // Check Retry-After header // Implement exponential backoff // Use token bucket for distributed apps

  • practice: Validate webhook signatures why: Prevents security vulnerabilities implementation: | const isValid = crypto .createHmac("sha256", clientSecret) .update(body) .digest("hex") === signature;

  • practice: Store and refresh OAuth tokens why: Tokens expire in 30 minutes implementation: | // Store refresh token securely // Refresh before expiry // Rotate every 6 months