Claude Code skill pack for HubSpot (30 skills)
Installation
Open Claude Code and run this command:
/plugin install hubspot-pack@claude-code-plugins-plus
Use --global to install for all projects, or --project for current project only.
Skills (30)
Debug complex HubSpot API issues with systematic isolation and evidence collection.
HubSpot Advanced Troubleshooting
Overview
Deep debugging techniques for complex HubSpot API issues: systematic layer testing, timing analysis, correlation ID tracking, and support escalation.
Prerequisites
- Access to application logs
curlandjqavailable- HubSpot access token for manual testing
Instructions
Step 1: Systematic Layer Testing
Test each layer independently to isolate the failure:
#!/bin/bash
# hubspot-layer-test.sh
echo "=== HubSpot Layer-by-Layer Diagnostic ==="
# Layer 1: DNS Resolution
echo "1. DNS Resolution"
dig api.hubapi.com +short || echo "FAIL: DNS resolution"
# Layer 2: TCP Connectivity
echo "2. TCP Connectivity"
timeout 5 bash -c 'echo > /dev/tcp/api.hubapi.com/443' 2>/dev/null \
&& echo "OK" || echo "FAIL: Cannot reach port 443"
# Layer 3: TLS Handshake
echo "3. TLS Handshake"
echo | openssl s_client -connect api.hubapi.com:443 2>/dev/null | grep "Verify return code"
# Layer 4: HTTP Response (unauthenticated)
echo "4. HTTP Response (no auth)"
curl -so /dev/null -w "HTTP %{http_code} in %{time_total}s\n" \
https://api.hubapi.com/crm/v3/objects/contacts?limit=1
# Layer 5: Authenticated Request
echo "5. Authenticated Request"
RESPONSE=$(curl -s -w "\n%{http_code}" \
https://api.hubapi.com/crm/v3/objects/contacts?limit=1 \
-H "Authorization: Bearer $HUBSPOT_ACCESS_TOKEN")
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | head -n -1)
echo "HTTP $HTTP_CODE"
if [ "$HTTP_CODE" = "200" ]; then
echo "OK: API accessible"
echo "Records: $(echo $BODY | jq '.results | length')"
else
echo "FAIL: $(echo $BODY | jq -r '.category // .message')"
echo "Correlation ID: $(echo $BODY | jq -r '.correlationId // "none"')"
fi
# Layer 6: Rate Limit State
echo "6. Rate Limit State"
curl -sI https://api.hubapi.com/crm/v3/objects/contacts?limit=1 \
-H "Authorization: Bearer $HUBSPOT_ACCESS_TOKEN" \
| grep -i "ratelimit" | sed 's/^/ /'
Step 2: Correlation ID Tracking
Every HubSpot error response includes a correlationId. Track these for support:
import * as hubspot from '@hubspot/api-client';
// Collect correlation IDs from errors
const errorLog: Array<{
timestamp: string;
correlationId: string;
statusCode: number;
category: string;
message: string;
endpoint: string;
}> = [];
async function debuggedApiCall<T>(
endpoint: string,
operation: () => Promise<T>
): Promise<T> {
try {
return await operation();
} catch (Choose and implement HubSpot integration architecture for different scales.
HubSpot Architecture Variants
Overview
Three validated architecture patterns for HubSpot CRM integrations at different scales, from embedded client to dedicated API gateway.
Prerequisites
- Understanding of team size and daily API call volume
- Knowledge of deployment infrastructure
- Clear sync requirements (real-time vs batch)
Instructions
Variant A: Embedded Client (Simple)
Best for: MVPs, small teams, < 10K contacts, < 50K API calls/day
your-app/
├── src/
│ ├── hubspot/
│ │ ├── client.ts # @hubspot/api-client singleton
│ │ └── contacts.ts # Direct CRM operations
│ ├── routes/
│ │ └── api.ts # API routes that call HubSpot directly
│ └── index.ts
// Direct integration in route handlers
import * as hubspot from '@hubspot/api-client';
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
numberOfApiCallRetries: 3,
});
app.get('/api/contacts', async (req, res) => {
const page = await client.crm.contacts.basicApi.getPage(
20, req.query.after as string, ['email', 'firstname', 'lastname']
);
res.json(page);
});
app.post('/api/contacts', async (req, res) => {
const contact = await client.crm.contacts.basicApi.create({
properties: req.body,
associations: [],
});
res.status(201).json(contact);
});
Pros: Fast to build, simple to understand, one deployment
Cons: No fault isolation, HubSpot latency in user request path
Variant B: Service Layer with Async Queue
Best for: Growing teams, 10K-100K contacts, 50K-300K API calls/day
your-app/
├── src/
│ ├── services/
│ │ └── hubspot/
│ │ ├── client.ts # Singleton with circuit breaker
│ │ ├── contact.ts # Business logic layer
│ │ ├── deal.ts
│ │ └── sync.ts # Background sync
│ ├── queue/
│ │ └── hubspot-worker.ts # Process async operations
│ ├── cache/
│ │ └── hubspot-cache.ts # Redis cache layer
│ ├── routes/
│ └── index.ts
// Service layer abstracts HubSpot from route handlers
class ContactService {
private client = getHubSpotClient();
private cache: Redis;
private queue: BullQueue;
async getContact(id: string): Promise<Contact> {
// Check cache first
const cached = await this.cache.get(`contact:${id}`);
if (cached) return JSON.parse(cached);
// Fetch from HubSpot
const contact = await this.client.crm.contacts.basicApi.getById(
id, ['email', 'firstname', 'lastname', 'lifecyclestage']
);
// Cache for 5 minutes
await this.cache.setex(`contact:${id}`, 300, JSON.stringify(contact)Configure CI/CD pipelines for HubSpot integrations with GitHub Actions.
HubSpot CI Integration
Overview
Set up GitHub Actions CI/CD for HubSpot integrations with unit tests, integration tests against a developer test account, and secret management.
Prerequisites
- GitHub repository with Actions enabled
- HubSpot developer test account token
- npm/pnpm project with test suite
Instructions
Step 1: Create GitHub Actions Workflow
# .github/workflows/hubspot-ci.yml
name: HubSpot Integration CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm test -- --coverage
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
integration-tests:
runs-on: ubuntu-latest
needs: unit-tests
# Only run integration tests on main branch pushes
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
env:
HUBSPOT_ACCESS_TOKEN: ${{ secrets.HUBSPOT_TEST_ACCESS_TOKEN }}
HUBSPOT_PORTAL_ID: ${{ secrets.HUBSPOT_TEST_PORTAL_ID }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Run HubSpot integration tests
run: HUBSPOT_TEST=true npm run test:integration
- name: Verify HubSpot connectivity
run: |
STATUS=$(curl -so /dev/null -w "%{http_code}" \
https://api.hubapi.com/crm/v3/objects/contacts?limit=1 \
-H "Authorization: Bearer $HUBSPOT_ACCESS_TOKEN")
echo "HubSpot API status: $STATUS"
[ "$STATUS" = "200" ] || exit 1
Step 2: Configure Secrets
# Store HubSpot test account credentials
gh secret set HUBSPOT_TEST_ACCESS_TOKEN --body "pat-na1-test-xxxxx"
gh secret set HUBSPOT_TEST_PORTAL_ID --body "12345678"
# For production deployments
gh secret set HUBSPOT_PROD_ACCESS_TOKEN --body "pat-na1-prod-xxxxx"
Step 3: Write CI-Friendly Tests
// tests/hubspot.integration.test.ts
import { describe, it, expect, afterAll } from 'vitest';
import * as hubspot from '@hubspot/api-client';
const shouldRun = process.env.HUBSPOT_TEST === 'true';
const createdIds: string[] = [];
describe.skipIf(!shouldRun)('HubSpot Integration', () => {
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
numberOfApiCallRetries: 3,
});
it('should list contacts', async () =&gDiagnose and fix common HubSpot API errors with real error responses.
HubSpot Common Errors
Overview
Quick reference for the most common HubSpot API errors, their real error response bodies, and solutions.
Prerequisites
@hubspot/api-clientinstalled- API credentials configured
- Access to application logs
Instructions
Step 1: Identify the Error
Check the HTTP status code and response body. HubSpot returns structured errors:
{
"status": "error",
"message": "One or more validation errors occurred",
"correlationId": "abc123-def456",
"category": "VALIDATION_ERROR",
"errors": [
{
"message": "Property values were not valid: [{\"isValid\":false,\"message\":\"...\"}]",
"context": { "propertyName": "email" }
}
]
}
Step 2: Match and Fix
401 Unauthorized
Real response:
{
"status": "error",
"message": "Authentication credentials not found. This API supports OAuth 2.0 authentication and you can find more details at https://developers.hubspot.com/docs/methods/auth/oauth-overview",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"category": "INVALID_AUTHENTICATION"
}
Causes:
- Missing
Authorization: Bearerheader - Expired OAuth access token (30-minute TTL)
- Revoked or regenerated private app token
Fix:
# Verify token is set and valid
curl -s https://api.hubapi.com/crm/v3/objects/contacts?limit=1 \
-H "Authorization: Bearer $HUBSPOT_ACCESS_TOKEN" | jq .status
# Should return null (success) or "error"
403 Forbidden
Real response:
{
"status": "error",
"message": "This access token does not have proper permissions. (requires any of [crm.objects.contacts.write])",
"correlationId": "...",
"category": "MISSING_SCOPES"
}
Fix: Add the missing scope to your private app in Settings > Integrations > Private Apps, then regenerate the token.
409 Conflict
Real response:
{
"status": "error",
"message": "Contact already exists. Existing ID: 12345",
"correlationId": "...",
"category": "CONFLICT"
}
Fix: Search before creating, or use batch upsert:
//Build a complete HubSpot CRM contact-to-deal pipeline workflow.
HubSpot Core Workflow A: Contact-to-Deal Pipeline
Overview
End-to-end workflow: capture a lead, create/update contact, create company, create deal in pipeline, advance deal stages, and log activities. The primary money-path workflow for HubSpot CRM.
Prerequisites
- Completed
hubspot-install-authsetup - Scopes:
crm.objects.contacts.write,crm.objects.companies.write,crm.objects.deals.write - Understanding of your HubSpot deal pipeline and stages
Instructions
Step 1: Capture and Upsert Contact
import * as hubspot from '@hubspot/api-client';
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
numberOfApiCallRetries: 3,
});
interface LeadInput {
email: string;
firstName: string;
lastName: string;
company: string;
phone?: string;
source?: string;
}
async function upsertContact(lead: LeadInput): Promise<string> {
// Search for existing contact by email
// POST /crm/v3/objects/contacts/search
const existing = await client.crm.contacts.searchApi.doSearch({
filterGroups: [{
filters: [{ propertyName: 'email', operator: 'EQ', value: lead.email }],
}],
properties: ['firstname', 'lastname', 'email'],
limit: 1,
after: 0,
sorts: [],
});
if (existing.results.length > 0) {
// Update existing contact
const contactId = existing.results[0].id;
await client.crm.contacts.basicApi.update(contactId, {
properties: {
firstname: lead.firstName,
lastname: lead.lastName,
phone: lead.phone || '',
hs_lead_status: 'NEW',
},
});
console.log(`Updated existing contact: ${contactId}`);
return contactId;
}
// Create new contact
const contact = await client.crm.contacts.basicApi.create({
properties: {
email: lead.email,
firstname: lead.firstName,
lastname: lead.lastName,
company: lead.company,
phone: lead.phone || '',
lifecyclestage: 'lead',
hs_lead_status: 'NEW',
},
associations: [],
});
console.log(`Created new contact: ${contact.id}`);
return contact.id;
}
Step 2: Find or Create Company
async function findOrCreateCompany(
domain: string,
name: string
): Promise<string> {
// Search by domain
const existing = await client.crm.companies.searchApi.doSearch({
filterGroups: [{
filters: [{ propertyName: 'domain', operator: 'EQ', value: domain }],
}],
properties: ['name', 'domain'],
limit: 1,
after: 0,
sorts: [],
});
if (existing.results.length > 0) {
return existing.results[0].id;
}
const company = await client.crm.companies.basicApi.create({
propeBuild HubSpot marketing automation with emails, forms, lists, and tickets.
HubSpot Core Workflow B: Marketing & Tickets
Overview
Marketing automation workflow: manage contact lists, process form submissions, send marketing emails, and create support tickets. Complements the sales pipeline in Workflow A.
Prerequisites
- Completed
hubspot-install-authsetup - Scopes:
crm.lists.read,crm.lists.write,content,forms,crm.objects.marketing.emails.read - Marketing Hub subscription (Starter+ for emails)
Instructions
Step 1: Create and Manage Contact Lists
import * as hubspot from '@hubspot/api-client';
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
numberOfApiCallRetries: 3,
});
// Create a static contact list
// POST /crm/v3/lists/
async function createStaticList(name: string): Promise<string> {
const response = await client.apiRequest({
method: 'POST',
path: '/crm/v3/lists/',
body: {
name,
objectTypeId: '0-1', // contacts
processingType: 'MANUAL', // static list
},
});
const data = await response.json();
return data.listId;
}
// Add contacts to a static list
// PUT /crm/v3/lists/{listId}/memberships/add
async function addToList(listId: string, contactIds: string[]): Promise<void> {
await client.apiRequest({
method: 'PUT',
path: `/crm/v3/lists/${listId}/memberships/add`,
body: contactIds.map(Number),
});
}
// Create a dynamic list with filter criteria
async function createDynamicList(name: string): Promise<string> {
const response = await client.apiRequest({
method: 'POST',
path: '/crm/v3/lists/',
body: {
name,
objectTypeId: '0-1',
processingType: 'DYNAMIC',
filterBranch: {
filterBranchType: 'AND',
filters: [
{
filterType: 'PROPERTY',
property: 'lifecyclestage',
operation: {
operationType: 'MULTISTRING',
operator: 'IS_ANY_OF',
values: ['lead', 'marketingqualifiedlead'],
},
},
],
},
},
});
const data = await response.json();
return data.listId;
}
Step 2: Process Form Submissions
// Handle a HubSpot form submission via the Forms API
// POST /submissions/v3/integration/secure/submit/{portalId}/{formGuid}
async function submitForm(
portalId: string,
formGuid: string,
fields: Record<string, string>,
context: { pageUri?: string; ipAddress?: string }
): Promise<void> {
await client.apiRequest({
method: 'POST',
path: `/submissions/v3/integration/secure/submit/${portalId}/${formGuid}`,
body: {
submittOptimize HubSpot costs through API call reduction, plan selection, and usage monitoring.
HubSpot Cost Tuning
Overview
Optimize HubSpot integration costs by reducing API call volume, monitoring usage against daily limits, and choosing the right plan.
Prerequisites
- Access to HubSpot account settings (Settings > Account > Usage & Limits)
- Understanding of current API usage patterns
Instructions
Step 1: Understand HubSpot API Pricing Model
HubSpot API calls are included with your subscription tier. There is no per-call billing, but exceeding limits results in 429 Too Many Requests errors that block your integration.
| Plan | Daily API Limit | Per-Second Limit |
|---|---|---|
| Free / Starter | 250,000 | 10 |
| Professional | 500,000 | 10 |
| Enterprise | 500,000 | 10 |
| API Limit Increase Add-on | 1,000,000 | 10 |
Key insight: The daily limit is per portal (shared across all apps). A poorly written integration can consume the entire quota and block all other apps.
Step 2: Monitor Current Usage
# Check rate limit headers on any API call
curl -sI https://api.hubapi.com/crm/v3/objects/contacts?limit=1 \
-H "Authorization: Bearer $HUBSPOT_ACCESS_TOKEN" \
| grep -i ratelimit
# Output:
# X-HubSpot-RateLimit-Daily: 500000
# X-HubSpot-RateLimit-Daily-Remaining: 487234
# X-HubSpot-RateLimit-Secondly: 10
# X-HubSpot-RateLimit-Secondly-Remaining: 9
// Programmatic usage tracking
class HubSpotUsageTracker {
private dailyCalls = 0;
private lastReset = new Date();
track(): void {
this.dailyCalls++;
// Reset counter at midnight
const now = new Date();
if (now.getDate() !== this.lastReset.getDate()) {
this.dailyCalls = 0;
this.lastReset = now;
}
}
getUsage(): { daily: number; percentUsed: number } {
const limit = parseInt(process.env.HUBSPOT_DAILY_LIMIT || '500000');
return {
daily: this.dailyCalls,
percentUsed: (this.dailyCalls / limit) * 100,
};
}
shouldAlert(): boolean {
return this.getUsage().percentUsed > 80;
}
}
Step 3: High-Impact Cost Reductions
Replace Individual Reads with Batch Reads
// BEFORE: 100 API calls
for (const id of contactIds) {
await client.crm.contacts.basicApi.getById(id, ['email']);
}
// AFTER: 1 API call (100x reduction)
await client.crm.contacts.batchApi.read({
inputs: contactIds.map(id => ({ id })),
properties: ['email'],
propertiesWithHistory: [],
});
Use Search Instead of List + Filter
// BEFORE: Fetch all, filter in memory (wastes API calls + bandwidth)
let after: string | undefinedImplement HubSpot GDPR compliance, data export, and contact privacy operations.
HubSpot Data Handling
Overview
Handle GDPR/CCPA compliance with HubSpot's built-in privacy APIs: GDPR delete, data export, consent management, and PII handling for CRM data.
Prerequisites
- HubSpot account with GDPR features enabled
- Scope:
crm.objects.contacts.write(for GDPR delete) - Understanding of GDPR/CCPA requirements
Instructions
Step 1: GDPR Contact Deletion
HubSpot provides a dedicated GDPR delete endpoint that permanently removes all contact data and communications:
import * as hubspot from '@hubspot/api-client';
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
});
// GDPR delete: permanently removes contact and all associated data
// POST /crm/v3/objects/contacts/gdpr-delete
async function gdprDeleteContact(email: string): Promise<void> {
// First, find the contact
const search = await client.crm.contacts.searchApi.doSearch({
filterGroups: [{
filters: [{ propertyName: 'email', operator: 'EQ', value: email }],
}],
properties: ['email'],
limit: 1, after: 0, sorts: [],
});
if (search.results.length === 0) {
console.log(`Contact not found: ${email}`);
return;
}
const contactId = search.results[0].id;
// GDPR delete via API
await client.apiRequest({
method: 'POST',
path: '/crm/v3/objects/contacts/gdpr-delete',
body: {
objectId: contactId,
idProperty: 'hs_object_id',
},
});
// Also delete from your local systems
await deleteLocalContactData(email);
// Audit log (keep for compliance -- do NOT delete audit records)
await auditLog({
action: 'GDPR_DELETE',
email: '[REDACTED]', // don't store the email in audit
contactId,
timestamp: new Date().toISOString(),
reason: 'Data subject deletion request',
});
console.log(`GDPR deleted contact ${contactId}`);
}
Step 2: Data Subject Access Request (DSAR)
Export all data HubSpot holds about a contact:
async function exportContactData(email: string): Promise<ContactDataExport> {
// Find contact
const search = await client.crm.contacts.searchApi.doSearch({
filterGroups: [{
filters: [{ propertyName: 'email', operator: 'EQ', value: email }],
}],
properties: [], // get all default properties
limit: 1, after: 0, sorts: [],
});
if (search.results.length === 0) {
return { found: false, data: null };
}
const contact = search.results[0];
// Get all properties for complete export
const fullContact = await client.crm.contacts.basicApi.getById(
contact.id,
undefined, // all properties
undefined,
['companies', 'deals', 'tickets'] // include associations
);
// Get assocCollect HubSpot debug evidence for support tickets and troubleshooting.
HubSpot Debug Bundle
Overview
Collect all necessary diagnostic information for HubSpot API troubleshooting and support ticket escalation, including correlation IDs, rate limit state, and SDK versions.
Prerequisites
@hubspot/api-clientinstalled- Access to application logs
HUBSPOTACCESSTOKENenvironment variable set
Instructions
Step 1: Create Debug Bundle Script
#!/bin/bash
# hubspot-debug-bundle.sh
BUNDLE_DIR="hubspot-debug-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BUNDLE_DIR"
echo "=== HubSpot Debug Bundle ===" > "$BUNDLE_DIR/summary.txt"
echo "Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$BUNDLE_DIR/summary.txt"
echo "" >> "$BUNDLE_DIR/summary.txt"
Step 2: Collect Environment and SDK Info
echo "--- Runtime ---" >> "$BUNDLE_DIR/summary.txt"
node --version >> "$BUNDLE_DIR/summary.txt" 2>&1
npm --version >> "$BUNDLE_DIR/summary.txt" 2>&1
echo "HUBSPOT_ACCESS_TOKEN: ${HUBSPOT_ACCESS_TOKEN:+[SET]}" >> "$BUNDLE_DIR/summary.txt"
echo "" >> "$BUNDLE_DIR/summary.txt"
# SDK version
echo "--- @hubspot/api-client ---" >> "$BUNDLE_DIR/summary.txt"
npm list @hubspot/api-client 2>/dev/null >> "$BUNDLE_DIR/summary.txt"
echo "" >> "$BUNDLE_DIR/summary.txt"
Step 3: Test API Connectivity and Rate Limits
echo "--- API Connectivity ---" >> "$BUNDLE_DIR/summary.txt"
# Test the API and capture headers
curl -sI https://api.hubapi.com/crm/v3/objects/contacts?limit=1 \
-H "Authorization: Bearer ${HUBSPOT_ACCESS_TOKEN}" \
> "$BUNDLE_DIR/api-headers.txt" 2>&1
# Extract key info
echo "HTTP Status: $(head -1 "$BUNDLE_DIR/api-headers.txt")" >> "$BUNDLE_DIR/summary.txt"
grep -i "x-hubspot-ratelimit" "$BUNDLE_DIR/api-headers.txt" >> "$BUNDLE_DIR/summary.txt"
grep -i "x-request-id" "$BUNDLE_DIR/api-headers.txt" >> "$BUNDLE_DIR/summary.txt"
echo "" >> "$BUNDLE_DIR/summary.txt"
# Test specific endpoints
for endpoint in contacts companies deals tickets; do
STATUS=$(curl -so /dev/null -w "%{http_code}" \
"https://api.hubapi.com/crm/v3/objects/${endpoint}?limit=1" \
-H "Authorization: Bearer ${HUBSPOT_ACCESS_TOKEN}")
echo "${endpoint}: HTTP ${STATUS}" >> "$BUNDLE_DIR/summary.txt"
done
echo "" >> "$BUNDLE_DIR/summary.txt"
# Check scopes via token info
echo "Deploy HubSpot integrations to Vercel, Fly.
HubSpot Deploy Integration
Overview
Deploy HubSpot-powered applications to Vercel, Fly.io, or Google Cloud Run with proper secret management and health checks.
Prerequisites
- HubSpot private app token for production
- Platform CLI installed (vercel, fly, or gcloud)
- Application code with health check endpoint
Instructions
Step 1: Vercel Deployment
# Add HubSpot secrets to Vercel
vercel env add HUBSPOT_ACCESS_TOKEN production
# Paste: pat-na1-xxxxx
# Optional webhook secret
vercel env add HUBSPOT_WEBHOOK_SECRET production
// vercel.json
{
"env": {
"HUBSPOT_ACCESS_TOKEN": "@hubspot-access-token"
},
"functions": {
"api/**/*.ts": {
"maxDuration": 30
}
}
}
// api/hubspot/contacts.ts (Vercel serverless function)
import * as hubspot from '@hubspot/api-client';
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
numberOfApiCallRetries: 3,
});
export default async function handler(req: Request) {
if (req.method === 'GET') {
const contacts = await client.crm.contacts.basicApi.getPage(
10, undefined, ['firstname', 'lastname', 'email']
);
return Response.json(contacts.results);
}
if (req.method === 'POST') {
const body = await req.json();
const contact = await client.crm.contacts.basicApi.create({
properties: body,
associations: [],
});
return Response.json(contact, { status: 201 });
}
}
# Deploy
vercel --prod
Step 2: Fly.io Deployment
# fly.toml
app = "my-hubspot-app"
primary_region = "iad"
[env]
NODE_ENV = "production"
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = true
auto_start_machines = true
[[http_service.checks]]
grace_period = "10s"
interval = "30s"
method = "GET"
path = "/health"
timeout = "5s"
# Set HubSpot secrets
fly secrets set HUBSPOT_ACCESS_TOKEN=pat-na1-xxxxx
fly secrets set HUBSPOT_WEBHOOK_SECRET=your-secret
# Deploy
fly deploy
# Verify health
fly status
curl https://my-hubspot-app.fly.dev/health
Step 3: Google Cloud Run Deployment
#!/bin/bash
# deploy-cloud-run.sh
PROJECT_ID="${GOOGLE_CLOUD_PROJECT}"
SERVICE_NAME="hubspot-service"
REGION="us-central1"
# Store token in Secret Manager
echo -n "pat-na1-xxxxx" | gcloud secrets create hubspot-access-token \
--data-file=- --replication-policy="automatic"
# Grant Cloud Run access to the secret
gcloConfigure HubSpot enterprise access control with OAuth scopes and team permissions.
HubSpot Enterprise RBAC
Overview
Implement role-based access control for HubSpot integrations using OAuth scopes, multiple private apps with different permissions, and application-level authorization.
Prerequisites
- HubSpot Enterprise subscription (for team-level permissions)
- Understanding of HubSpot OAuth scopes
- Multiple private apps or OAuth app configured
Instructions
Step 1: Scope-Based Access Model
HubSpot's permission model is scope-based. Create separate private apps for different access levels:
| Role | Private App | Scopes | Use Case |
|---|---|---|---|
| Reader | hubspot-readonly |
crm.objects.contacts.read, crm.objects.deals.read, crm.objects.companies.read |
Dashboards, reports |
| Writer | hubspot-readwrite |
Above + .write variants |
CRM operations |
| Admin | hubspot-admin |
All CRM scopes + crm.schemas.*.read |
Schema management |
| Sync | hubspot-sync |
crm.objects.contacts.read, crm.objects.contacts.write, crm.import |
Data sync jobs |
| Webhook | hubspot-webhook |
automation |
Event handling only |
Step 2: Multi-Token Client Factory
import * as hubspot from '@hubspot/api-client';
type AccessLevel = 'reader' | 'writer' | 'admin' | 'sync';
const TOKEN_MAP: Record<AccessLevel, string> = {
reader: process.env.HUBSPOT_READER_TOKEN!,
writer: process.env.HUBSPOT_WRITER_TOKEN!,
admin: process.env.HUBSPOT_ADMIN_TOKEN!,
sync: process.env.HUBSPOT_SYNC_TOKEN!,
};
const clientCache = new Map<AccessLevel, hubspot.Client>();
export function getClientForRole(role: AccessLevel): hubspot.Client {
if (!clientCache.has(role)) {
const token = TOKEN_MAP[role];
if (!token) {
throw new Error(`No token configured for role: ${role}`);
}
clientCache.set(role, new hubspot.Client({
accessToken: token,
numberOfApiCallRetries: 3,
}));
}
return clientCache.get(role)!;
}
// Usage
const readClient = getClientForRole('reader'); // can only read
const writeClient = getClientForRole('writer'); // can read and write
Step 3: Application-Level Permission Middleware
import { Request, Response, NextFunction } from 'express';
interface AppPermissions {
contacts: { read: boolean; write: boolean; delete: boolean };
deals: { read: boolean; write: boolean; delete: boolean };
companies: { read: boolean; write: boolean; Create a working HubSpot CRM example with contacts, companies, and deals.
HubSpot Hello World
Overview
Create, read, update, and delete CRM records using the HubSpot API. Covers contacts, companies, and deals with real endpoints and SDK methods.
Prerequisites
- Completed
hubspot-install-authsetup - Private app with scopes:
crm.objects.contacts.read,crm.objects.contacts.write @hubspot/api-clientinstalled
Instructions
Step 1: Create a Contact
import * as hubspot from '@hubspot/api-client';
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
});
// POST /crm/v3/objects/contacts
const contactResponse = await client.crm.contacts.basicApi.create({
properties: {
firstname: 'Jane',
lastname: 'Doe',
email: 'jane.doe@example.com',
phone: '(555) 123-4567',
company: 'Acme Corp',
lifecyclestage: 'lead',
},
associations: [],
});
console.log(`Created contact: ${contactResponse.id}`);
// Output: Created contact: 12345
Step 2: Read a Contact
// GET /crm/v3/objects/contacts/{contactId}
const contact = await client.crm.contacts.basicApi.getById(
contactResponse.id,
['firstname', 'lastname', 'email', 'phone', 'lifecyclestage'],
undefined, // propertiesWithHistory
['companies'] // associations to include
);
console.log(`${contact.properties.firstname} ${contact.properties.lastname}`);
console.log(`Email: ${contact.properties.email}`);
console.log(`Stage: ${contact.properties.lifecyclestage}`);
Step 3: Update a Contact
// PATCH /crm/v3/objects/contacts/{contactId}
const updated = await client.crm.contacts.basicApi.update(
contactResponse.id,
{
properties: {
lifecyclestage: 'marketingqualifiedlead',
phone: '(555) 987-6543',
},
}
);
console.log(`Updated at: ${updated.updatedAt}`);
Step 4: Create a Company and Associate
// POST /crm/v3/objects/companies
const company = await client.crm.companies.basicApi.create({
properties: {
name: 'Acme Corp',
domain: 'acme.com',
industry: 'TECHNOLOGY',
numberofemployees: '150',
annualrevenue: '5000000',
},
associations: [],
});
// Associate contact with company
// PUT /crm/v4/objects/contacts/{contactId}/associations/companies/{companyId}
await client.crm.associations.v4.basicApi.create(
'contacts',
contactResponse.id,
'companies',
company.id,
[{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 1 }]
);
console.log(`Associated contact ${contactResponse.id} with company ${company.id}`);
Step 5: Create a Deal
Execute HubSpot incident response with triage, mitigation, and postmortem.
HubSpot Incident Runbook
Overview
Rapid incident response procedures for HubSpot CRM integration failures, including triage, mitigation, and postmortem templates.
Prerequisites
- Access to application logs and metrics
HUBSPOTACCESSTOKENavailable for manual testing- Communication channels (Slack, PagerDuty)
Instructions
Step 1: Quick Triage (< 2 minutes)
#!/bin/bash
# hubspot-triage.sh -- Run this first during any incident
echo "=== HubSpot Quick Triage ==="
echo "Time: $(date -u)"
# 1. Is HubSpot itself down?
echo ""
echo "--- HubSpot Platform Status ---"
curl -s https://status.hubspot.com/api/v2/summary.json | jq '{
status: .status.description,
active_incidents: [.incidents[] | {name, status, updated_at}]
}'
# 2. Can we reach the API?
echo ""
echo "--- API Connectivity ---"
STATUS=$(curl -so /dev/null -w "%{http_code}" \
https://api.hubapi.com/crm/v3/objects/contacts?limit=1 \
-H "Authorization: Bearer $HUBSPOT_ACCESS_TOKEN")
echo "API Status: HTTP $STATUS"
# 3. Rate limit state
echo ""
echo "--- Rate Limits ---"
curl -sI https://api.hubapi.com/crm/v3/objects/contacts?limit=1 \
-H "Authorization: Bearer $HUBSPOT_ACCESS_TOKEN" \
| grep -i "ratelimit\|retry-after"
# 4. Check our health endpoint
echo ""
echo "--- Our Health Check ---"
curl -sf https://your-app.com/health | jq '.services.hubspot' || echo "Health check failed"
Step 2: Decision Tree
Is HubSpot status page showing an incident?
├── YES → HubSpot-side outage
│ ├── Enable fallback/degraded mode
│ ├── Notify stakeholders: "HubSpot platform issue"
│ └── Monitor status page for resolution
└── NO → Our integration issue
├── Is the error 401/403?
│ ├── YES → Token revoked or regenerated
│ │ └── Get new token from Settings > Private Apps
│ └── NO → Continue diagnosis
├── Is the error 429?
│ ├── YES → Rate limit exceeded
│ │ ├── Check if another app is consuming quota
│ │ └── Reduce request volume or wait for reset
│ └── NO → Continue diagnosis
├── Is the error 5xx?
│ ├── YES → HubSpot transient error (not on status page)
│ │ └── SDK retries should handle this (numberOfApiCallRetries)
│ └── NO → Application bug
└── Check application logs for the real error
Step 3: Common Incident Responses
Token Revoked (401)
# Verify current token
curl -s https://api.hubapi.com/crm/v3/objects/contacts?limit=1 \
-H "Authorization: Bearer $HUBSPOT_ACCESS_TOKEN" | jq .category
# If "INVALID_AUTHENTICATION":
# 1. Go to HubSpot Settings > Integrations > Private Apps
Install and configure HubSpot API client with authentication.
HubSpot Install & Auth
Overview
Set up the @hubspot/api-client SDK and configure authentication using private app access tokens or OAuth 2.0.
Prerequisites
- Node.js 18+ or Python 3.10+
- HubSpot account (free or paid)
- Private app created in Settings > Integrations > Private Apps
- Required scopes selected for your private app
Instructions
Step 1: Install the SDK
# Node.js (official SDK)
npm install @hubspot/api-client
# Python
pip install hubspot-api-client
Step 2: Create a Private App in HubSpot
- Go to Settings > Integrations > Private Apps
- Click "Create a private app"
- Name your app and select scopes:
crm.objects.contacts.read/crm.objects.contacts.writecrm.objects.companies.read/crm.objects.companies.writecrm.objects.deals.read/crm.objects.deals.writecrm.objects.custom.read/crm.objects.custom.writecrm.schemas.contacts.read(for properties)
- Copy the generated access token
Step 3: Configure Environment
# .env file (add to .gitignore)
HUBSPOT_ACCESS_TOKEN=pat-na1-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# .gitignore
echo '.env' >> .gitignore
echo '.env.local' >> .gitignore
Step 4: Initialize and Verify
import * as hubspot from '@hubspot/api-client';
const hubspotClient = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN,
});
// Verify connection by fetching account info
async function verifyConnection() {
try {
const response = await hubspotClient.crm.contacts.basicApi.getPage(
1, // limit
undefined, // after
['firstname', 'lastname', 'email']
);
console.log(`Connected. Found ${response.results.length} contact(s).`);
return true;
} catch (error) {
if (error.code === 401) {
console.error('Invalid access token. Check HUBSPOT_ACCESS_TOKEN.');
} else if (error.code === 403) {
console.error('Missing scopes. Add crm.objects.contacts.read to your private app.');
} else {
console.error('Connection failed:', error.message);
}
return false;
}
}
Step 5: OAuth 2.0 Setup (Public Apps)
// For public apps distributed to multiple HubSpot portals
const hubspotClient = new hubspot.Client();
// Step 1: Generate authorization URL
const authUrl = hubspotClient.oauth.getAuthorizationUrl(
'your-client-id',
'http://localhost:3000/oauth/callback',
'crm.objects.contacts.read crm.objects.contacts.write'
);
// Redirect userIdentify and avoid HubSpot API anti-patterns and common integration mistakes.
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,
[Load test HubSpot integrations and plan capacity around API rate limits.
HubSpot Load & Scale
Overview
Load testing and capacity planning for HubSpot integrations, constrained by the 10 requests/second and 500,000 requests/day API limits.
Prerequisites
- k6 or similar load testing tool
- HubSpot developer test account (never load test against production)
- Understanding of HubSpot rate limits
Instructions
Step 1: Understand HubSpot Rate Limit Constraints
Your integration's maximum throughput is bound by HubSpot's limits:
| Constraint | Limit | Impact |
|---|---|---|
| Per-second | 10 req/sec | 600 req/min maximum |
| Daily | 500,000/day | ~347 req/min sustained |
| Batch size | 100 records/batch | Each batch = 1 API call |
| Search results | 10,000 total | Cannot page past 10K |
| Associations | 500 per record | Hard limit |
Effective throughput with batching:
- Individual operations: 10 records/sec
- Batch operations: 1,000 records/sec (10 batches/sec x 100 records/batch)
Step 2: k6 Load Test Script
// hubspot-load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
const errorRate = new Rate('hubspot_errors');
const rateLimited = new Rate('hubspot_rate_limited');
export const options = {
stages: [
{ duration: '1m', target: 2 }, // warm up (2 req/sec)
{ duration: '3m', target: 5 }, // moderate load
{ duration: '2m', target: 8 }, // approach limit
{ duration: '2m', target: 10 }, // at limit
{ duration: '1m', target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ['p(95)<2000'], // 95% under 2s
hubspot_errors: ['rate<0.05'], // <5% errors
hubspot_rate_limited: ['rate<0.10'], // <10% rate limited
},
};
const BASE_URL = 'https://api.hubapi.com';
const TOKEN = __ENV.HUBSPOT_ACCESS_TOKEN;
export default function () {
// Test: List contacts (GET)
const listRes = http.get(
`${BASE_URL}/crm/v3/objects/contacts?limit=10&properties=email,firstname`,
{ headers: { Authorization: `Bearer ${TOKEN}` } }
);
check(listRes, { 'list contacts: 200': (r) => r.status === 200 });
errorRate.add(listRes.status >= 400 && listRes.status !== 429);
rateLimited.add(listRes.status === 429);
sleep(0.1); // 100ms between requests per VU
// Test: Search contacts (POST)
const searchRes = http.post(
`${BASE_URL}/crm/v3/objects/contacts/search`,
JSON.stringify({
filterGroups: [{
filters: [{
propertyName: 'lifConfigure HubSpot local development with testing and sandbox accounts.
HubSpot Local Dev Loop
Overview
Set up a fast local development workflow for HubSpot integrations with sandbox accounts, mocking, and test utilities.
Prerequisites
- Completed
hubspot-install-authsetup - Node.js 18+ with npm/pnpm
- HubSpot developer test account (free at developers.hubspot.com)
Instructions
Step 1: Create Project Structure
my-hubspot-project/
├── src/
│ ├── hubspot/
│ │ ├── client.ts # Singleton @hubspot/api-client wrapper
│ │ ├── contacts.ts # Contact operations
│ │ ├── deals.ts # Deal operations
│ │ └── types.ts # HubSpot type definitions
│ └── index.ts
├── tests/
│ ├── mocks/
│ │ └── hubspot.ts # Shared mock factory
│ ├── contacts.test.ts
│ └── deals.test.ts
├── .env.local # Local secrets (git-ignored)
├── .env.example # Template for team
├── tsconfig.json
└── package.json
Step 2: Create Client Singleton
// src/hubspot/client.ts
import * as hubspot from '@hubspot/api-client';
let instance: hubspot.Client | null = null;
export function getHubSpotClient(): hubspot.Client {
if (!instance) {
if (!process.env.HUBSPOT_ACCESS_TOKEN) {
throw new Error('HUBSPOT_ACCESS_TOKEN environment variable is required');
}
instance = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN,
numberOfApiCallRetries: 3,
});
}
return instance;
}
// Reset client (useful for tests)
export function resetHubSpotClient(): void {
instance = null;
}
Step 3: Configure Testing with Vitest
{
"scripts": {
"dev": "tsx watch src/index.ts",
"test": "vitest",
"test:watch": "vitest --watch",
"test:integration": "HUBSPOT_TEST=true vitest --config vitest.integration.config.ts"
},
"devDependencies": {
"@hubspot/api-client": "^13.0.0",
"vitest": "^2.0.0",
"tsx": "^4.0.0"
}
}
Step 4: Mock HubSpot API for Unit Tests
// tests/mocks/hubspot.ts
import { vi } from 'vitest';
export function createMockHubSpotClient() {
return {
crm: {
contacts: {
basicApi: {
create: vi.fn().mockResolvedValue({
id: '101',
properties: { firstname: 'Jane', lastname: 'Doe', email: 'jane@test.com' },
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
archived: false,
}),
getById: vi.fn().mockResolvedValue({
id: '101',
properties: { firstname: 'Jane', lastname: 'Doe'Execute CRM data migration to HubSpot with batch imports and validation.
HubSpot Migration Deep Dive
Overview
Comprehensive guide for migrating CRM data into HubSpot, including data mapping, batch imports via API, validation, and rollback procedures.
Prerequisites
- Source CRM data exported (CSV or API access)
- HubSpot account with required scopes
- Custom properties created in HubSpot for non-default fields
Instructions
Step 1: Data Inventory and Mapping
// Map source CRM fields to HubSpot properties
interface FieldMapping {
sourceField: string;
hubspotProperty: string;
transform?: (value: string) => string;
required: boolean;
}
const contactFieldMap: FieldMapping[] = [
{ sourceField: 'Email', hubspotProperty: 'email', required: true },
{ sourceField: 'First Name', hubspotProperty: 'firstname', required: false },
{ sourceField: 'Last Name', hubspotProperty: 'lastname', required: false },
{ sourceField: 'Phone', hubspotProperty: 'phone', required: false },
{ sourceField: 'Company', hubspotProperty: 'company', required: false },
{
sourceField: 'Lead Status',
hubspotProperty: 'lifecyclestage',
transform: (val) => {
// Map source values to HubSpot lifecycle stages
const map: Record<string, string> = {
'New': 'lead',
'Qualified': 'marketingqualifiedlead',
'Won': 'customer',
};
return map[val] || 'lead';
},
required: false,
},
];
function mapRecord(
source: Record<string, string>,
fieldMap: FieldMapping[]
): Record<string, string> {
const mapped: Record<string, string> = {};
for (const field of fieldMap) {
const value = source[field.sourceField];
if (value !== undefined && value !== '') {
mapped[field.hubspotProperty] = field.transform ? field.transform(value) : value;
} else if (field.required) {
throw new Error(`Missing required field: ${field.sourceField}`);
}
}
return mapped;
}
Step 2: Create Custom Properties Before Import
import * as hubspot from '@hubspot/api-client';
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
});
// Create custom properties that don't exist in HubSpot
async function ensureCustomProperties(objectType: string) {
const customProps = [
{
name: 'source_crm_id',
label: 'Source CRM ID',
type: 'string',
fieldType: 'text',
groupName: 'contactinformation',
description: 'Original record ID from source CRM',
},
{
name: 'migration_date',
label: 'Migration Date',
type: 'date',
fieldType: 'date',
groupConfigure HubSpot across development, staging, and production environments.
HubSpot Multi-Environment Setup
Overview
Configure HubSpot integrations across dev/staging/production using separate HubSpot portals (test accounts), environment-specific tokens, and configuration management.
Prerequisites
- HubSpot developer account (for test portals)
- Secret management solution (Vault, AWS SM, GCP SM)
- CI/CD pipeline with environment variables
Instructions
Step 1: Environment Strategy
| Environment | HubSpot Portal | Token Type | Data |
|---|---|---|---|
| Development | Developer test account | Test private app token | Fake/seed data |
| Staging | Sandbox portal | Staging private app token | Anonymized copy |
| Production | Production portal | Production private app token | Real customer data |
Create a free developer test account at developers.hubspot.com.
Step 2: Environment Configuration
// src/config/hubspot.ts
import * as hubspot from '@hubspot/api-client';
type Environment = 'development' | 'staging' | 'production';
interface HubSpotEnvConfig {
accessToken: string;
portalId: string;
retries: number;
cacheTtlMs: number;
}
const ENV_CONFIG: Record<Environment, Partial<HubSpotEnvConfig>> = {
development: {
retries: 1,
cacheTtlMs: 0, // no cache in dev for fresh data
},
staging: {
retries: 3,
cacheTtlMs: 60000,
},
production: {
retries: 5,
cacheTtlMs: 300000,
},
};
function getEnvironment(): Environment {
const env = process.env.NODE_ENV || 'development';
if (['development', 'staging', 'production'].includes(env)) {
return env as Environment;
}
return 'development';
}
export function getHubSpotConfig(): HubSpotEnvConfig {
const env = getEnvironment();
return {
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
portalId: process.env.HUBSPOT_PORTAL_ID!,
...ENV_CONFIG[env],
} as HubSpotEnvConfig;
}
export function createHubSpotClient(): hubspot.Client {
const config = getHubSpotConfig();
return new hubspot.Client({
accessToken: config.accessToken,
numberOfApiCallRetries: config.retries,
});
}
Step 3: Per-Environment Secrets
# .env.development (git-ignored)
HUBSPOT_ACCESS_TOKEN=pat-na1-test-xxxxx # developer test account
HUBSPOT_PORTAL_ID=12345678
# .env.staging (git-ignored)
HUBSPOT_ACCESS_TOKEN=pat-na1-staging-xxxxx # sandbox portal
HUBSPOT_PORTAL_ID=23456789
# .env.production (NEVER in files -- use secret manager)
# Stored in AWS Secrets Manager / GCP Secret Manager / Vault
# AWS Secrets ManagSet up observability for HubSpot integrations with metrics, traces, and alerts.
HubSpot Observability
Overview
Instrument HubSpot API calls with Prometheus metrics, OpenTelemetry tracing, and structured logging to monitor CRM integration health.
Prerequisites
- Prometheus or compatible metrics backend
- OpenTelemetry SDK (optional, for tracing)
- Structured logging library (pino recommended)
Instructions
Step 1: Prometheus Metrics
import { Counter, Histogram, Gauge, Registry } from 'prom-client';
const registry = new Registry();
const hubspotRequests = new Counter({
name: 'hubspot_api_requests_total',
help: 'Total HubSpot API requests',
labelNames: ['method', 'object_type', 'status'],
registers: [registry],
});
const hubspotLatency = new Histogram({
name: 'hubspot_api_request_duration_seconds',
help: 'HubSpot API request duration',
labelNames: ['method', 'object_type'],
buckets: [0.05, 0.1, 0.25, 0.5, 1, 2, 5],
registers: [registry],
});
const hubspotRateLimit = new Gauge({
name: 'hubspot_rate_limit_remaining',
help: 'HubSpot daily rate limit remaining',
labelNames: ['type'],
registers: [registry],
});
const hubspotErrors = new Counter({
name: 'hubspot_api_errors_total',
help: 'HubSpot API errors by category',
labelNames: ['status_code', 'category'],
registers: [registry],
});
Step 2: Instrumented Client Wrapper
import * as hubspot from '@hubspot/api-client';
class InstrumentedHubSpotClient {
private client: hubspot.Client;
constructor() {
this.client = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
numberOfApiCallRetries: 3,
});
}
async tracked<T>(
method: string,
objectType: string,
operation: () => Promise<T>
): Promise<T> {
const timer = hubspotLatency.startTimer({ method, object_type: objectType });
try {
const result = await operation();
hubspotRequests.inc({ method, object_type: objectType, status: 'success' });
return result;
} catch (error: any) {
const statusCode = error?.code || error?.statusCode || 500;
const category = error?.body?.category || 'UNKNOWN';
hubspotRequests.inc({ method, object_type: objectType, status: 'error' });
hubspotErrors.inc({ status_code: String(statusCode), category });
throw error;
} finally {
timer();
}
}
// Example: instrumented contact operations
async getContact(id: string, properties: string[]) {
return this.tracked('GET', 'contacts', () =>
this.client.crm.contacts.basicApi.getById(id, properties)
);
}
async createContact(properties: Record<string, string>) {
return this.tracked('POST'Optimize HubSpot API performance with caching, batching, and search optimization.
HubSpot Performance Tuning
Overview
Optimize HubSpot API performance through batch operations, caching, search optimization, and request minimization.
Prerequisites
@hubspot/api-clientinstalled- Understanding of your access patterns (read-heavy vs write-heavy)
- Optional: Redis for distributed caching
Instructions
Step 1: Use Batch APIs Everywhere
The single biggest performance win: batch operations reduce API calls by up to 100x.
import * as hubspot from '@hubspot/api-client';
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
numberOfApiCallRetries: 3,
});
// BAD: 100 API calls for 100 contacts
async function getContactsSlow(ids: string[]) {
return Promise.all(ids.map(id =>
client.crm.contacts.basicApi.getById(id, ['email', 'firstname'])
));
}
// GOOD: 1 API call for 100 contacts
async function getContactsFast(ids: string[], properties: string[]) {
// POST /crm/v3/objects/contacts/batch/read
const result = await client.crm.contacts.batchApi.read({
inputs: ids.map(id => ({ id })),
properties,
propertiesWithHistory: [],
});
return result.results;
}
// For more than 100 records, chunk:
async function getContactsChunked(ids: string[], properties: string[]) {
const results = [];
for (let i = 0; i < ids.length; i += 100) {
const chunk = ids.slice(i, i + 100);
const batch = await getContactsFast(chunk, properties);
results.push(...batch);
}
return results;
}
Step 2: Request Only Needed Properties
// BAD: Returns ALL default properties (slow, large payload)
const contact = await client.crm.contacts.basicApi.getById(id);
// GOOD: Only request what you need (fast, small payload)
const contact = await client.crm.contacts.basicApi.getById(
id,
['email', 'firstname', 'lastname', 'lifecyclestage'] // specific properties
);
Step 3: Cache Frequently Accessed Data
import { LRUCache } from 'lru-cache';
const contactCache = new LRUCache<string, any>({
max: 5000, // max entries
ttl: 5 * 60 * 1000, // 5 minute TTL
updateAgeOnGet: true,
});
async function getCachedContact(id: string, properties: string[]) {
const cacheKey = `contact:${id}`;
const cached = contactCache.get(cacheKey);
if (cached) return cached;
const contact = await client.crm.contacts.basicApi.getById(id, properties);
contactCache.set(cacheKey, contact);
return contact;
}
// Invalidate on webhook events
function invalidateContactCache(contactId: string): void {
contactCache.delete(`contact:${contactId}`);
}
Step 4: Optimize Search Queries
// BAD: Overly broad search, returns too Implement HubSpot lint rules, secret scanning, and CI policy checks.
HubSpot Policy & Guardrails
Overview
Automated policy enforcement for HubSpot integrations: secret scanning, ESLint rules, CI checks for token leaks, and runtime guardrails.
Prerequisites
- ESLint configured in project
- CI/CD pipeline (GitHub Actions)
- TypeScript for compile-time enforcement
Instructions
Step 1: Secret Scanning (Prevent Token Leaks)
# .github/workflows/hubspot-security.yml
name: HubSpot Security Scan
on: [push, pull_request]
jobs:
secret-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Scan for HubSpot private app tokens
run: |
# Pattern: pat-{region}{number}-{uuid}
if grep -rE "pat-[a-z]{2}[0-9]-[a-f0-9-]{36}" \
--include="*.ts" --include="*.js" --include="*.json" --include="*.yaml" \
--exclude-dir=node_modules --exclude-dir=.git .; then
echo "::error::HubSpot private app token found in source code!"
echo "Rotate this token immediately in HubSpot Settings > Private Apps"
exit 1
fi
- name: Scan for deprecated API keys
run: |
# Pattern: hapikey=uuid
if grep -rE "hapikey=[a-f0-9-]{36}" \
--include="*.ts" --include="*.js" --include="*.env.example" \
--exclude-dir=node_modules .; then
echo "::error::Deprecated HubSpot API key found! Migrate to private app tokens."
exit 1
fi
- name: Verify .gitignore includes .env files
run: |
if ! grep -q "^\.env$" .gitignore; then
echo "::error::.gitignore missing .env entry"
exit 1
fi
Step 2: ESLint Rule -- No Deprecated API Key Auth
// eslint-rules/no-hubspot-api-key.js
module.exports = {
meta: {
type: 'problem',
docs: { description: 'Disallow deprecated HubSpot API key authentication' },
messages: {
noApiKey: 'HubSpot API keys are deprecated. Use accessToken from a private app instead.',
useAccessToken: 'Use { accessToken: process.env.HUBSPOT_ACCESS_TOKEN } instead of { apiKey }',
},
},
create(context) {
return {
Property(node) {
if (
node.key.type === 'Identifier' &&
node.key.name === 'apiKey' &&
node.parent?.parent?.callee?.name === 'Client'
) {
context.report({ node, messageId: 'noApiKey' });
}
},
Literal(node) {
if (typeof node.value === 'string') {
// Detect hardcoded private app tokens
if (node.value.match(/^pat-[a-z]{2}\d-[a-f0-9-]{36}$/)) {
conExecute HubSpot production deployment checklist and go-live procedures.
HubSpot Production Checklist
Overview
Complete checklist for deploying HubSpot CRM integrations to production with health checks, monitoring, and rollback procedures.
Prerequisites
- Staging environment tested and verified
- Production private app token with minimal scopes
- Deployment pipeline configured
- Monitoring/alerting ready
Instructions
Step 1: Pre-Deployment Verification
- [ ] Production private app created with minimal scopes
- [ ] Access token stored in secret manager (not env file)
- [ ]
.envfiles in.gitignore - [ ] No hardcoded tokens in source (
grep -r "pat-na1" src/) - [ ] Webhook endpoints use HTTPS only
- [ ] Webhook signature verification implemented (v3)
- [ ] Error handling covers 401, 403, 404, 409, 429, 5xx
- [ ] Rate limiting/backoff implemented (
numberOfApiCallRetries: 3) - [ ] Batch operations used where possible (max 100/batch)
- [ ] All tests passing against developer test account
Step 2: Health Check Endpoint
import * as hubspot from '@hubspot/api-client';
interface HealthCheckResult {
status: 'healthy' | 'degraded' | 'unhealthy';
hubspot: {
connected: boolean;
latencyMs: number;
rateLimitRemaining?: number;
};
timestamp: string;
}
async function hubspotHealthCheck(): Promise<HealthCheckResult> {
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
});
const start = Date.now();
try {
// Cheapest possible API call: fetch 1 contact
await client.crm.contacts.basicApi.getPage(1);
return {
status: 'healthy',
hubspot: {
connected: true,
latencyMs: Date.now() - start,
},
timestamp: new Date().toISOString(),
};
} catch (error: any) {
const status = error?.code || error?.statusCode || 500;
return {
status: status === 429 ? 'degraded' : 'unhealthy',
hubspot: {
connected: false,
latencyMs: Date.now() - start,
},
timestamp: new Date().toISOString(),
};
}
}
// Express endpoint
app.get('/health', async (req, res) => {
const result = await hubspotHealthCheck();
const httpStatus = result.status === 'healthy' ? 200 :
result.status === 'degraded' ? 200 : 503;
res.status(httpStatus).json(result);
});
Step 3: Monitoring Alerts
| Alert | Condition | Severity | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| HubSpot unreachable | Health check fails 3x | P1 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| High error rate | 5xx errors > 10/min | P1 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Auth failure | Any 401/403 response | P1 (token revoked?) |
| Plan | Per-Second Limit | Daily Limit | Burst |
|---|---|---|---|
| Free/Starter | 10 requests/sec | 250,000/day | -- |
| Professional | 10 requests/sec | 500,000/day | -- |
| Enterprise | 10 requests/sec | 500,000/day | -- |
| API Add-on | 10 requests/sec | 1,000,000/day | -- |
Critical: Limits are per HubSpot portal (account), not per app. All private apps and OAuth apps in the same portal share the same limit bucket.
Step 2: Use SDK Built-in Retries
import * as hubspot from '@hubspot/api-client';
// The SDK has built-in retry for 429 responses
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
numberOfApiCallRetries: 3, // retries 429 and 5xx automatically
});
Step 3: Custom Backoff with Retry-After Header
async function withHubSpotBackoff<T>(
operation: () => Promise<T>,
config = { maxRetries: 5, baseDelayMs: 1000, maxDelayMs: 30000 }
): Promise<T> {
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
try {
return await operation();
} catch (error: any) {
if (attempt === config.maxRetries) throw error;
const status = error?.code || error?.statusCode || error?.response?.status;
// Only retry on 429 and 5xx
if (status !== 429 && (status < 500 || status >= 600)) throw error;
// Honor Retry-After header from HubSpot
let delay: number;
const retryAfter = error?.response?.headers?.['retry-after'];
if (retryAfter) {
delay = parseInt(retryAfter) * 1000;
} else {
// Exponential backoff with jitter
const exponential = config.baseDelayMs * Math.pow(2, attempt);
const jitter = Math.random() * 500;
delay = Math.min(exponential + jitter, config.maxDelayMs);
}
console.warn(`HubSpot rate limited (attempt ${attempt + 1}/${config.maxRetries}). ` +
`Retrying in ${delay}ms...`);
await new Promise(r => setTimeout(r, delay));
}
}
throw new Error('Unreachable');
}
Step 4: Request Queue for Throughput Control
import PQueue from 'p-queue';
// Queue that respects HubSpot's 10 req/sec limit
Implement a production-ready HubSpot integration architecture with layered design.
HubSpot Reference Architecture
Overview
Production-ready layered architecture for HubSpot CRM integrations with typed clients, service abstraction, caching, and webhook handling.
Prerequisites
- TypeScript 5+ project
@hubspot/api-clientv13+ installed- Understanding of layered architecture patterns
Instructions
Step 1: Project Structure
my-hubspot-integration/
├── src/
│ ├── hubspot/ # HubSpot infrastructure layer
│ │ ├── client.ts # Singleton @hubspot/api-client wrapper
│ │ ├── types.ts # HubSpot-specific types
│ │ ├── errors.ts # Error classification
│ │ ├── cache.ts # Response caching
│ │ └── associations.ts # Association type constants
│ ├── services/ # Business logic layer
│ │ ├── contact.service.ts # Contact CRUD + business rules
│ │ ├── deal.service.ts # Deal pipeline operations
│ │ ├── company.service.ts # Company management
│ │ └── sync.service.ts # Data synchronization
│ ├── api/ # API layer
│ │ ├── routes/
│ │ │ ├── contacts.ts # REST endpoints
│ │ │ ├── deals.ts
│ │ │ └── webhooks.ts # Webhook receiver
│ │ └── middleware/
│ │ ├── auth.ts # Request auth
│ │ └── webhook-verify.ts # HubSpot signature verification
│ ├── jobs/ # Background jobs
│ │ ├── sync-contacts.ts # Scheduled sync
│ │ └── process-webhooks.ts # Async event processing
│ └── index.ts
├── tests/
│ ├── unit/
│ │ ├── services/
│ │ └── mocks/hubspot.ts # Shared mock factory
│ └── integration/
│ └── hubspot.integration.test.ts
├── config/
│ ├── default.ts # Shared config
│ └── production.ts # Production overrides
└── package.json
Step 2: Infrastructure Layer
// src/hubspot/client.ts
import * as hubspot from '@hubspot/api-client';
let instance: hubspot.Client | null = null;
export function getHubSpotClient(): hubspot.Client {
if (!instance) {
instance = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
numberOfApiCallRetries: 3,
});
}
return instance;
}
// src/hubspot/associations.ts
// Default association type IDs (HUBSPOT_DEFINED category)
export const ASSOCIATION_TYPES = {
CONTACT_TO_COMPANY: 1,
CONTACT_TO_DEAL: 3,
COMPANY_TO_DEAL: 5,
CONTACT_TO_TICKET: 16,
NOTE_TO_CONTACT: 202,
TASK_TO_CONTACT: 204,
NOTE_TO_DEAL: 214,
} as const;
// src/hubspot/errors.ts
export class HubSpotError extends Error {
constructor(
message: string,
public readonly statusCode: number,
public readonly category: string,
public readonly correlationId: string,
public readonly retryable: boolean
Implement HubSpot reliability patterns: circuit breakers, retries, and graceful degradation.
HubSpot Reliability Patterns
Overview
Production-grade reliability patterns for HubSpot CRM integrations: circuit breaker, retry with Retry-After, graceful degradation, and dead letter queues.
Prerequisites
@hubspot/api-clientinstalled (has built-in retry)- Optional:
opossumfor circuit breaker - Optional: Redis or database for dead letter queue
Instructions
Step 1: SDK Built-in Retry (First Line of Defense)
import * as hubspot from '@hubspot/api-client';
// The SDK automatically retries 429 and 5xx errors
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
numberOfApiCallRetries: 3, // retries with exponential backoff
});
// This handles most transient failures automatically
Step 2: Circuit Breaker for HubSpot API
import CircuitBreaker from 'opossum';
// Circuit breaker wrapping all HubSpot API calls
const hubspotBreaker = new CircuitBreaker(
async <T>(operation: () => Promise<T>): Promise<T> => operation(),
{
timeout: 15000, // 15s timeout per call
errorThresholdPercentage: 50, // open after 50% failure rate
resetTimeout: 30000, // try again after 30s
volumeThreshold: 5, // need 5+ calls before evaluating
rollingCountTimeout: 60000, // 60s rolling window
}
);
// Monitor circuit state
hubspotBreaker.on('open', () => {
console.warn('[HubSpot] Circuit OPEN -- requests failing fast');
// Alert team: HubSpot integration degraded
});
hubspotBreaker.on('halfOpen', () => {
console.info('[HubSpot] Circuit HALF-OPEN -- testing recovery');
});
hubspotBreaker.on('close', () => {
console.info('[HubSpot] Circuit CLOSED -- normal operation');
});
// Usage
async function resilientHubSpotCall<T>(operation: () => Promise<T>): Promise<T> {
return hubspotBreaker.fire(operation) as Promise<T>;
}
// Example
const contacts = await resilientHubSpotCall(() =>
client.crm.contacts.basicApi.getPage(10, undefined, ['email'])
);
Step 3: Graceful Degradation
// Serve cached/fallback data when HubSpot is unavailable
import { LRUCache } from 'lru-cache';
const fallbackCache = new LRUCache<string, any>({
max: 10000,
ttl: 30 * 60 * 1000, // 30 minutes
});
async function withFallback<T>(
cacheKey: string,
operation: () => Promise<T>,
fallback?: T
): Promise<{ data: T; source: 'live' | 'cache' | 'fallback' }> {
try {
const data = await resilientHubSpotCall(operation);
fallbackCache.set(cacheKey, data);
return { data, source: 'live' };
} catch (error) {
// Try cache first
Apply production-ready @hubspot/api-client SDK patterns for TypeScript.
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-clientv13+ 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 aApply HubSpot security best practices for tokens, scopes, and webhook verification.
HubSpot Security Basics
Overview
Security best practices for HubSpot private app tokens, OAuth scopes, webhook signature verification, and secret management.
Prerequisites
- HubSpot private app or OAuth app configured
- Understanding of environment variables and secret management
Instructions
Step 1: Least-Privilege Scopes
Only request the scopes your integration actually uses:
| Use Case | Required Scopes |
|---|---|
| Read contacts | crm.objects.contacts.read |
| Write contacts | crm.objects.contacts.read, crm.objects.contacts.write |
| Read/write deals | crm.objects.deals.read, crm.objects.deals.write |
| Marketing emails | content |
| Forms | forms |
| Contact lists | crm.lists.read, crm.lists.write |
| Properties | crm.schemas.contacts.read |
| Custom objects | crm.objects.custom.read, crm.objects.custom.write, crm.schemas.custom.read |
| Webhooks | automation |
Never use: Do not grant all scopes. If you regenerate a private app token, the old token is immediately revoked.
Step 2: Token Storage
# .env (NEVER commit)
HUBSPOT_ACCESS_TOKEN=pat-na1-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
HUBSPOT_WEBHOOK_SECRET=your-webhook-secret
# .gitignore
.env
.env.local
.env.*.local
// Validate token is present at startup
function validateConfig(): void {
if (!process.env.HUBSPOT_ACCESS_TOKEN) {
throw new Error('HUBSPOT_ACCESS_TOKEN is required. See .env.example');
}
// Never log the token
console.log('HubSpot: Token configured', {
prefix: process.env.HUBSPOT_ACCESS_TOKEN.substring(0, 8) + '...',
});
}
Step 3: Webhook Signature Verification (v3)
HubSpot sends webhooks with signature verification headers:
import crypto from 'crypto';
import express from 'express';
// HubSpot v3 signature verification
// Header: X-HubSpot-Signature-v3
function verifyHubSpotSignatureV3(
requestBody: string,
signature: string,
timestamp: string,
clientSecret: string,
requestUri: string,
method: string = 'POST'
): boolean {
// Reject if timestamp is older than 5 minutes (replay protection)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
console.warn('HubSpot webhook timestamp too old');
return false;
}
// v3: HMAC SHA-256 oUpgrade @hubspot/api-client SDK versions and migrate between API versions.
HubSpot Upgrade & Migration
Overview
Guide for upgrading @hubspot/api-client SDK versions and migrating from legacy HubSpot APIs to the current CRM v3 API.
Prerequisites
- Current
@hubspot/api-clientinstalled - Git for version control
- Test suite available
- Staging environment for validation
Instructions
Step 1: Check Current Version and Available Updates
# Current version
npm list @hubspot/api-client
# Latest available
npm view @hubspot/api-client version
# See changelog
npm view @hubspot/api-client versions --json | tail -10
Step 2: Review Breaking Changes
Key breaking changes in @hubspot/api-client:
| Version | Breaking Change | Migration |
|---|---|---|
| v11 -> v12 | Association APIs moved to v4 namespace | client.crm.associations.v4.basicApi |
| v10 -> v11 | Batch API input format changed | Wrap inputs in { inputs: [...] } |
| v9 -> v10 | apiKey auth removed (API keys deprecated) |
Use accessToken only |
| v8 -> v9 | TypeScript strict types on all methods | Update type imports |
Step 3: Create Upgrade Branch and Update
git checkout -b chore/upgrade-hubspot-api-client
npm install @hubspot/api-client@latest
npm test
Step 4: Common Migration Patterns
API Key to Access Token (v9 -> v10+)
// BEFORE (deprecated -- API keys removed in v10)
const client = new hubspot.Client({ apiKey: 'your-api-key' });
// AFTER (use private app access token)
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
});
Associations v3 to v4 (v11 -> v12+)
// BEFORE (v3 associations)
await client.crm.contacts.associationsApi.create(
contactId, 'companies', companyId, 'contact_to_company'
);
// AFTER (v4 associations)
await client.crm.associations.v4.basicApi.create(
'contacts', contactId, 'companies', companyId,
[{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 1 }]
);
Legacy Contact API to CRM v3
// BEFORE (legacy /contacts/v1/)
const response = await fetch(
`https://api.hubapi.com/contacts/v1/contact/email/${email}/profile`,
{ headers: { Authorization: `Bearer ${token}` } }
);
// AFTER (CRM v3 search)
const result = await client.crm.contacts.searchApi.doSearch({
filterGroups: [{
filters: [{ propertyName: 'email', operator: 'EQ', value: email }],
}],
properties: ['firstname', 'lastname', &Implement HubSpot webhook subscriptions and CRM event handling.
HubSpot Webhooks & Events
Overview
Set up HubSpot webhook subscriptions for CRM events (contact/company/deal creation, updates, deletions) with v3 signature verification and idempotent event handling.
Prerequisites
- HubSpot public app (webhooks require a public app, not a private app)
- Client secret from your app settings (for signature verification)
- HTTPS endpoint accessible from the internet
- Optional: Redis or database for idempotency
Instructions
Step 1: Understand HubSpot Webhook Events
HubSpot sends webhook events as batches of CRM change notifications:
[
{
"eventId": 100,
"subscriptionId": 1234,
"portalId": 12345678,
"appId": 98765,
"occurredAt": 1711234567890,
"subscriptionType": "contact.propertyChange",
"attemptNumber": 0,
"objectId": 123,
"propertyName": "lifecyclestage",
"propertyValue": "marketingqualifiedlead",
"changeSource": "CRM",
"sourceId": "userId:12345"
}
]
Available subscription types:
contact.creation,contact.deletion,contact.propertyChange,contact.privacyDeletioncompany.creation,company.deletion,company.propertyChangedeal.creation,deal.deletion,deal.propertyChangeticket.creation,ticket.deletion,ticket.propertyChangecontact.merge,company.merge,deal.mergecontact.associationChange,company.associationChange,deal.associationChange
Step 2: Set Up Webhook Endpoint with Signature Verification
import express from 'express';
import crypto from 'crypto';
const app = express();
// IMPORTANT: Use raw body for signature verification
app.post('/webhooks/hubspot',
express.raw({ type: 'application/json' }),
async (req, res) => {
// Verify signature (v3)
const signature = req.headers['x-hubspot-signature-v3'] as string;
const timestamp = req.headers['x-hubspot-request-timestamp'] as string;
if (!signature || !timestamp) {
// Fall back to v2 signature
const sigV2 = req.headers['x-hubspot-signature'] as string;
if (!verifySignatureV2(req.body.toString(), sigV2)) {
return res.status(401).json({ error: 'Invalid signature' });
}
} else {
const requestUri = `https://${req.headers.host}${req.originalUrl}`;
if (!verifySignatureV3(req.body.toString(), signature, Ready to use hubspot-pack?
Related Plugins
excel-analyst-pro
Professional financial modeling toolkit for Claude Code with auto-invoked Skills and Excel MCP integration. Build DCF models, LBO analysis, variance reports, and pivot tables using natural language.
brand-strategy-framework
A 7-part brand strategy framework for building comprehensive brand foundations - the same methodology top agencies use with Fortune 500 clients.
clay-pack
Complete Clay integration skill pack with 30 skills covering data enrichment, waterfall workflows, AI agents, and GTM automation. Flagship+ tier vendor pack.
instantly-pack
Complete Instantly integration skill pack with 24 skills covering cold email, outreach automation, and lead generation. Flagship tier vendor pack.
apollo-pack
Complete Apollo integration skill pack with 24 skills covering sales engagement, prospecting, sequencing, analytics, and outbound automation. Flagship tier vendor pack.
juicebox-pack
Complete Juicebox integration skill pack with 24 skills covering people data, enrichment, contact search, and AI-powered discovery. Flagship tier vendor pack.