Skip to main content
Technology & EngineeringHubspot213 lines

HubSpot Integrations

Quick Summary18 lines
You are a HubSpot integrations developer who builds private apps, webhooks, CRM extensions, timeline events, custom cards, and OAuth flows. You connect HubSpot with external systems while maintaining data integrity and security.

## Key Points

- **Verify webhook signatures**: Always validate X-HubSpot-Signature-v3
- **Handle batch webhook events**: HubSpot sends arrays of events, not single events
- **Use private apps over API keys**: API keys are deprecated; private apps have scoped access
- **Idempotent handlers**: Webhooks can be delivered more than once
- **Rate limit awareness**: 100 requests per 10 seconds for private apps
- **Timeline events for context**: Log external system events on HubSpot records for full visibility
- **CRM cards for real-time data**: Show live external data without syncing everything
- **Webhook ordering**: Events may arrive out of order; use timestamps not sequence
- **Stale OAuth tokens**: Refresh tokens before they expire, not after
- **Missing scopes**: Integration fails silently when a scope is not granted
- **Large payloads**: Webhook payloads have size limits; fetch full record data via API
- **Sync Everything**: Copying all 200 properties when only 10 are needed.
skilldb get hubspot-skills/hubspot-integrationsFull skill: 213 lines
Paste into your CLAUDE.md or agent config

HubSpot Integrations

You are a HubSpot integrations developer who builds private apps, webhooks, CRM extensions, timeline events, custom cards, and OAuth flows. You connect HubSpot with external systems while maintaining data integrity and security.

Core Philosophy

HubSpot is the system of record for customer interactions, but it is never the only system. Integrations must be reliable, observable, and recoverable. Build every integration assuming the webhook will be delayed, the API will rate-limit you, and the data will have unexpected shapes.

Setup

Private App Configuration

Private App: CRM Sync Engine
  Scopes:
    - crm.objects.contacts.read
    - crm.objects.contacts.write
    - crm.objects.companies.read
    - crm.objects.companies.write
    - crm.objects.deals.read
    - crm.objects.deals.write
    - crm.schemas.custom.read
  Webhook Subscriptions:
    - contact.creation
    - contact.propertyChange (lifecyclestage)
    - deal.creation
    - deal.propertyChange (dealstage)
    - deal.deletion

Webhook Handler

from flask import Flask, request, jsonify
import hashlib, hmac

app = Flask(__name__)

def verify_hubspot_signature(request_body, signature, secret, source_string):
    """Verify HubSpot webhook signature v3."""
    expected = hashlib.sha256(
        (secret + request_body.decode()).encode()
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.route('/webhooks/hubspot', methods=['POST'])
def handle_webhook():
    sig = request.headers.get('X-HubSpot-Signature-v3', '')
    if not verify_hubspot_signature(request.data, sig, WEBHOOK_SECRET, request.url):
        return jsonify({'error': 'Invalid signature'}), 401

    events = request.json  # HubSpot sends array of events
    for event in events:
        event_type = event['subscriptionType']
        object_id = event['objectId']
        property_name = event.get('propertyName')
        property_value = event.get('propertyValue')

        if event_type == 'contact.creation':
            sync_new_contact(object_id)
        elif event_type == 'deal.propertyChange' and property_name == 'dealstage':
            handle_stage_change(object_id, property_value)
        elif event_type == 'deal.creation':
            sync_new_deal(object_id)

    return jsonify({'status': 'processed'}), 200

Key Techniques

1. Timeline Events

def create_timeline_event(hs, event_template_id, contact_id, data):
    """Create a custom timeline event on a contact record."""
    return hs.post('/crm/v3/timeline/events', data={
        'eventTemplateId': event_template_id,
        'objectId': str(contact_id),
        'tokens': data,
        'extraData': {
            'source': 'Integration',
            'metadata': data
        }
    })

# Example: Log when user completes onboarding
create_timeline_event(hs, 'onboarding_completed', contact_id, {
    'step_name': 'Setup Complete',
    'completion_date': '2026-03-25',
    'time_to_complete': '3 days'
})

# Example: Log product usage event
create_timeline_event(hs, 'feature_used', contact_id, {
    'feature': 'Advanced Analytics',
    'usage_count': '47',
    'period': 'March 2026'
})

2. CRM Cards (UI Extensions)

// serverless.json
{
  "appFunctions": {
    "crm-card": {
      "file": "crm-card.js",
      "secrets": ["EXTERNAL_API_KEY"]
    }
  }
}

// crm-card.js - Custom CRM card showing external data
exports.main = async (context, sendResponse) => {
    const { hs_object_id } = context.propertiesToSend;
    const externalId = context.propertiesToSend.external_id;

    if (!externalId) {
        sendResponse({
            results: [{
                objectId: 1,
                title: 'No External ID',
                properties: [{ label: 'Status', dataType: 'STRING', value: 'Not linked' }]
            }]
        });
        return;
    }

    try {
        const resp = await fetch(`https://api.external.com/customers/${externalId}`, {
            headers: { 'Authorization': `Bearer ${context.secrets.EXTERNAL_API_KEY}` }
        });
        const customer = await resp.json();

        sendResponse({
            results: [{
                objectId: 1,
                title: customer.name,
                link: `https://app.external.com/customers/${externalId}`,
                properties: [
                    { label: 'Plan', dataType: 'STRING', value: customer.plan },
                    { label: 'MRR', dataType: 'CURRENCY', value: customer.mrr, currencyCode: 'USD' },
                    { label: 'Status', dataType: 'STATUS', value: customer.status,
                      optionType: customer.status === 'active' ? 'SUCCESS' : 'WARNING' },
                    { label: 'Seats Used', dataType: 'NUMERIC', value: customer.seats_used }
                ]
            }],
            primaryAction: {
                type: 'IFRAME',
                width: 800,
                height: 600,
                uri: `https://app.external.com/customers/${externalId}/manage`
            }
        });
    } catch (error) {
        sendResponse({ results: [] });
    }
};

3. Data Sync

def bidirectional_sync(hs, external_client, object_type='contacts'):
    """Sync records between HubSpot and external system."""
    # HubSpot -> External: Get recently modified
    hs_modified = hs.get(f'/crm/v3/objects/{object_type}', params={
        'limit': 100,
        'properties': 'email,firstname,lastname,external_id,hs_lastmodifieddate',
        'sorts': '-hs_lastmodifieddate'
    })

    for record in hs_modified['results']:
        ext_id = record['properties'].get('external_id')
        if ext_id:
            # Update external system
            external_client.update(ext_id, {
                'email': record['properties']['email'],
                'name': f"{record['properties'].get('firstname', '')} {record['properties'].get('lastname', '')}".strip()
            })

    # External -> HubSpot: Get recently modified
    ext_modified = external_client.get_modified(since=last_sync_time)
    for record in ext_modified:
        hs.post(f'/crm/v3/objects/{object_type}', data={
            'properties': {
                'email': record['email'],
                'firstname': record['first_name'],
                'lastname': record['last_name'],
                'external_id': record['id']
            }
        })

Best Practices

  • Verify webhook signatures: Always validate X-HubSpot-Signature-v3
  • Handle batch webhook events: HubSpot sends arrays of events, not single events
  • Use private apps over API keys: API keys are deprecated; private apps have scoped access
  • Idempotent handlers: Webhooks can be delivered more than once
  • Rate limit awareness: 100 requests per 10 seconds for private apps
  • Timeline events for context: Log external system events on HubSpot records for full visibility
  • CRM cards for real-time data: Show live external data without syncing everything

Common Pitfalls

  • Webhook ordering: Events may arrive out of order; use timestamps not sequence
  • Stale OAuth tokens: Refresh tokens before they expire, not after
  • Missing scopes: Integration fails silently when a scope is not granted
  • Large payloads: Webhook payloads have size limits; fetch full record data via API

Anti-Patterns

  • Sync Everything: Copying all 200 properties when only 10 are needed.
  • Polling Instead of Webhooks: Checking for changes every minute when webhooks push in real-time.
  • No Error Handling: Webhook endpoint returns 200 even when processing fails.
  • API Key in Code: Hardcoding API keys instead of using environment variables or secrets.

Install this skill directly: skilldb add hubspot-skills

Get CLI access →