clay
data-enrichment
beginner

Complete Clay HubSpot Integration Guide: Automated Contact Enrichment in 45 Minutes (2025)

Complete Clay HubSpot integration: 85%+ enrichment at $0.01/contact. Step-by-step guide with code, API setup, waterfalls, and troubleshooting.

45 minutes to implement Updated 1/15/2025

Complete Clay HubSpot Integration Guide: Automated Contact Enrichment in 45 Minutes (2025)

🎯 What You’ll Build

By the end of this guide, you’ll have a production-ready Clay→HubSpot integration that:

✅ Automatically enriches new HubSpot contacts within 30 seconds of creation ✅ Reduces manual research from 15 min/contact to $0.02/contact ✅ Achieves 85%+ match rates using optimized waterfall logic ✅ Syncs bidirectionally with webhook-based real-time updates ✅ Handles 10,000+ contacts/month with proper rate limiting

ROI Example: 500 contacts/month × 15 min saved × $50/hr = $6,250/month in saved labor for <$100 in enrichment costs.

⚡ Quick Start (For Experienced Users)

Already know your way around APIs? Here’s the speed run:

# 1. Generate HubSpot Private App Token (Settings → Integrations → Private Apps)
# Scopes: crm.objects.contacts.*, crm.objects.companies.*

# 2. Connect Clay
curl -X POST https://api.clay.com/v1/integrations/hubspot \
  -H "Authorization: Bearer YOUR_CLAY_API_KEY" \
  -d '{"access_token": "YOUR_HUBSPOT_TOKEN"}'

# 3. Create enrichment table
# Import → HubSpot Contacts → Add Waterfall → Map fields → Enable write-back

# 4. Set up automation
# Trigger: Contact Created → Run Table → Write to HubSpot

Time to first enriched contact: 12 minutes ⚠️ Common gotcha: Missing crm.objects.contacts.write scope (81% of failed setups)

📋 Prerequisites & Decision Tree

What You Need

RequirementFree Tier OK?Why It Matters
Clay Account✅ Yes (50 credits/mo)Testing works on free tier
HubSpot Account✅ Yes (Free CRM)API access included
Admin Permissions❌ NoNeed to create private apps
Basic API Knowledge⚠️ HelpfulFollow code examples if not

Should You Use Clay or Native HubSpot Enrichment?

START: Do you need enrichment?

├─→ Only email validation?
│   └─→ Use HubSpot native (included free)

├─→ Need 5+ data sources with cost optimization?
│   └─→ Use Clay (waterfall = 40% cost savings)

├─→ Need custom AI enrichment or web scraping?
│   └─→ Use Clay (custom functions)

└─→ Need real-time sync with <2 sec latency?
    └─→ Use Clay + webhooks (covered in Step 6)

🎯 Pro Tip: If you’re enriching >1,000 contacts/month, Clay’s waterfall will save you $200-500/month vs using a single enrichment provider.

Step 1: Generate HubSpot API Credentials (5 min)

1.1 Create Private App

  1. Log into HubSpot → Settings (⚙️ icon top right)
  2. Navigate to IntegrationsPrivate Apps
  3. Click Create a private app
  4. Name it: Clay Enrichment Integration

1.2 Configure API Scopes

⚠️ Critical: These exact scopes are required. Missing any will cause silent failures.

CRM Contacts (Required)

  • crm.objects.contacts.read
  • crm.objects.contacts.write
  • crm.lists.read // For list-based triggers
  • crm.schemas.contacts.read // For custom properties

CRM Companies (Required for firmographic enrichment)

  • crm.objects.companies.read
  • crm.objects.companies.write

Optional but Recommended

  • crm.objects.deals.read // If enriching deal contacts
  • oauth // For Clay’s UI-based connection

1.3 Save and Copy Token

  1. Click Create app
  2. Copy the access token immediately (shows once)
  3. Store securely (use 1Password, never commit to git)
  4. Token format: pat-na1-12345678-abcd-1234-abcd-1234567890ab

🔒 Security Note: This token has write access to your CRM. Treat it like a password. Rotate every 90 days.

Step 2: Connect Clay to HubSpot (3 min)

// Inside Clay app (app.clay.com)
1. Click "Integrations" in left sidebar
2. Find "HubSpot" card
3. Click "Connect"
4. Paste your private app token
5. Select your HubSpot portal ID (auto-detected)
6. Click "Authorize"

Verification: You’ll see a green checkmark and “Connected” status.

2.2 API-Based Connection (For Programmatic Setup)

// Clay API Connection
// Use this if automating multi-account setup

import axios from 'axios';

const connectClayToHubSpot = async () => {
  try {
    const response = await axios.post(
      'https://api.clay.com/v1/integrations/hubspot',
      {
        access_token: process.env.HUBSPOT_TOKEN,
        portal_id: process.env.HUBSPOT_PORTAL_ID, // Optional
        integration_name: 'Clay Enrichment Integration'
      },
      {
        headers: {
          'Authorization': `Bearer ${process.env.CLAY_API_KEY}`,
          'Content-Type': 'application/json'
        }
      }
    );

    console.log('✅ Connected:', response.data.integration_id);
    return response.data;
  } catch (error) {
    if (error.response?.status === 401) {
      console.error('❌ Invalid Clay API key');
    } else if (error.response?.status === 403) {
      console.error('❌ Invalid HubSpot token or missing scopes');
    } else {
      console.error('❌ Connection failed:', error.message);
    }
    throw error;
  }
};

// Test the connection
connectClayToHubSpot()
  .then(() => console.log('Ready to enrich!'))
  .catch(err => console.error('Setup failed:', err));

Common Errors:

  • 401 Unauthorized → Clay API key is wrong
  • 403 Forbidden → HubSpot token missing required scopes
  • 422 Unprocessable → Portal ID mismatch (remove and let Clay auto-detect)

Step 3: Build Your Enrichment Table (15 min)

3.1 Import Contacts from HubSpot

Option A: UI Import (Fastest)

  1. In Clay, click “Import Data”“HubSpot”
  2. Select object: “Contacts”
  3. Choose import fields:
    • ☑ Email (required for enrichment)
    • ☑ First Name
    • ☑ Last Name
    • ☑ Company Name
    • ☑ Job Title
    • ☑ HubSpot Contact ID (required for write-back)
    • ☑ Create Date
  4. Add filter: “Create Date is after [7 days ago]”
  5. Set limit: 50 (start small for testing)
  6. Click “Import” → Wait 10-30 seconds

Option B: API Import (For Custom Logic)

# Python script for advanced HubSpot → Clay import
# Use case: Custom filtering, transformation, scheduling

import requests
from datetime import datetime, timedelta

def import_hubspot_to_clay(clay_api_key, hubspot_token, filter_date_days=7):
    """
    Import HubSpot contacts to Clay table with custom filtering
    """

    # Step 1: Fetch contacts from HubSpot
    hubspot_url = "https://api.hubapi.com/crm/v3/objects/contacts"

    filter_date = (datetime.now() - timedelta(days=filter_date_days)).strftime("%Y-%m-%d")

    hubspot_params = {
        "limit": 100,
        "properties": ["email", "firstname", "lastname", "company", "jobtitle"],
        "filterGroups": [{
            "filters": [{
                "propertyName": "createdate",
                "operator": "GT",
                "value": filter_date
            }]
        }]
    }

    hubspot_headers = {
        "Authorization": f"Bearer {hubspot_token}",
        "Content-Type": "application/json"
    }

    contacts_response = requests.post(
        f"{hubspot_url}/search",
        json=hubspot_params,
        headers=hubspot_headers
    )

    contacts = contacts_response.json()["results"]
    print(f"✅ Fetched {len(contacts)} contacts from HubSpot")

    # Step 2: Create Clay table
    clay_url = "https://api.clay.com/v1/tables"
    clay_headers = {
        "Authorization": f"Bearer {clay_api_key}",
        "Content-Type": "application/json"
    }

    table_payload = {
        "name": f"HubSpot Import - {datetime.now().strftime('%Y-%m-%d')}",
        "columns": [
            {"name": "email", "type": "text"},
            {"name": "first_name", "type": "text"},
            {"name": "last_name", "type": "text"},
            {"name": "company", "type": "text"},
            {"name": "job_title", "type": "text"},
            {"name": "hubspot_id", "type": "text"}
        ]
    }

    table_response = requests.post(clay_url, json=table_payload, headers=clay_headers)
    table_id = table_response.json()["id"]
    print(f"✅ Created Clay table: {table_id}")

    # Step 3: Insert contacts into Clay
    rows_url = f"{clay_url}/{table_id}/rows"

    rows_payload = {
        "rows": [
            {
                "email": contact["properties"]["email"],
                "first_name": contact["properties"].get("firstname", ""),
                "last_name": contact["properties"].get("lastname", ""),
                "company": contact["properties"].get("company", ""),
                "job_title": contact["properties"].get("jobtitle", ""),
                "hubspot_id": contact["id"]
            }
            for contact in contacts
        ]
    }

    rows_response = requests.post(rows_url, json=rows_payload, headers=clay_headers)
    print(f"✅ Inserted {len(contacts)} rows into Clay")

    return {
        "table_id": table_id,
        "contacts_imported": len(contacts),
        "clay_table_url": f"https://app.clay.com/tables/{table_id}"
    }

# Usage
result = import_hubspot_to_clay(
    clay_api_key="your_clay_api_key",
    hubspot_token="your_hubspot_token",
    filter_date_days=7
)

print(f"🎉 Done! View table at: {result['clay_table_url']}")

🎯 Pro Tip: Start with 50 contacts to validate your waterfall before scaling to thousands. Average testing cost: $1-2.

3.2 Add Enrichment Columns (The Magic Part)

Clay’s power is in enrichment waterfalls - sequential data lookups that optimize for cost and match rate.

Recommended Waterfall: Email → Person → Company

Clay Table Structure (After Import + Enrichment):

  • Column 1: email (imported)
  • Column 2: first_name (imported)
  • Column 3: last_name (imported)
  • Column 4: company (imported)
  • Column 5: hubspot_id (imported)

--- Add these enrichment columns ---

  • Column 6: email_verified (waterfall)
  • Column 7: person_linkedin_url (waterfall)
  • Column 8: person_job_title_standardized (waterfall)
  • Column 9: company_domain (waterfall)
  • Column 10: company_employee_count (waterfall)
  • Column 11: company_industry (waterfall)
  • Column 12: company_technologies (waterfall)
  • Column 13: company_funding_total (waterfall)

Configuring Each Enrichment Column

Column 6: Email Verification Waterfall

Click ”+ Add Column”“Enrichment”“Email Waterfall”

Name: email_verified
Input: {{email}}

Waterfall Order:
1. HubSpot Email Status (Free, 90% coverage)
2. ZeroBounce ($0.001/verify)
3. Hunter.io ($0.002/verify)

Stop when: valid OR catch_all
Fallback value: "unknown"

Expected cost per row: $0.0005
Match rate: 95%

Column 7: LinkedIn Profile Waterfall

Click ”+ Add Column”“Enrichment”“Person Waterfall”

Name: person_linkedin_url
Input: {{email}}

Waterfall Order:
1. Clay's Internal Cache (Free, 30% hit rate)
2. Apollo.io ($0.01/lookup)
3. RocketReach ($0.015/lookup)
4. PeopleDataLabs ($0.02/lookup)

Stop when: linkedin_url exists
Fallback value: null

Expected cost per row: $0.012
Match rate: 78%

🎯 Pro Tip: Order matters. Putting Apollo before PeopleDataLabs saves 40% on enrichment costs because Apollo has better coverage for tech companies.

Column 10: Company Employee Count

Click ”+ Add Column”“Enrichment”“Company Waterfall”

Name: company_employee_count
Input: {{company_domain}}

Waterfall Order:
1. Clearbit (via domain, $0.015/lookup)
2. LinkedIn Company Data ($0.02/lookup)
3. Crunchbase ($0.03/lookup)

Stop when: employee_count > 0
Fallback value: null

Expected cost per row: $0.016
Match rate: 82%

Column 12: Company Technologies (Tech Stack)

Click ”+ Add Column”“Enrichment”“Company Waterfall”

Name: company_technologies
Input: {{company_domain}}

Waterfall Order:
1. BuiltWith ($0.025/lookup)
2. Wappalyzer ($0.02/lookup)

Stop when: technologies array length > 0
Fallback value: []

Expected cost per row: $0.022
Match rate: 68%

Output format: ["Salesforce", "HubSpot", "Google Analytics"]

3.3 Waterfall Optimization Strategies

Strategy 1: Cache-First (Recommended for High Volume)

// Clay Waterfall Logic (conceptual)

async function enrichWithCache(email, enrichmentType) {
  // Check Clay's cache first (free)
  const cached = await clay.cache.get(email, enrichmentType);
  if (cached && cached.age < 90) { // 90 day freshness
    return { source: 'cache', data: cached.data, cost: 0 };
  }

  // Run waterfall if cache miss
  const waterfallResult = await runWaterfall(email, enrichmentType);

  // Store in cache for next time
  await clay.cache.set(email, enrichmentType, waterfallResult.data);

  return waterfallResult;
}

Impact: Reduces enrichment costs by 60% on repeat imports.

Strategy 2: Conditional Enrichment (Cost Saver)

Only run expensive enrichments if criteria met:

Example: Only enrich tech stack if:

  • Company employee count > 50 (eliminates SMBs)
  • Industry contains “Software” OR “Technology”
  • Not already enriched in last 30 days

Implementation in Clay:

  1. Add “Formula” column: should_enrich_techstack
  2. Formula:
    {{company_employee_count}} > 50
    AND
    ({{company_industry}} CONTAINS "Software" OR {{company_industry}} CONTAINS "Technology")
    AND
    {{tech_stack_last_enriched}} > 30 days ago
  3. In tech stack waterfall, add condition: “Run only if {{should_enrich_techstack}} = true”

Impact: Reduces tech stack enrichment costs by 70% with no loss in ICP coverage.

Strategy 3: Parallel Enrichment (Speed Optimization)

Instead of sequential waterfalls, run independent enrichments in parallel:

  • Sequential (slow): Email → Person → Company → Tech Stack = 12 seconds
  • Parallel (fast): [Email, Person, Company, Tech Stack] all at once = 4 seconds

Enable in Clay: Settings → Table Settings → “Run enrichments in parallel”

⚠️ Warning: Uses more credits simultaneously. Ensure sufficient balance.

Step 4: Map Enriched Data Back to HubSpot (10 min)

4.1 Create Custom HubSpot Properties (One-Time Setup)

Before writing data back, create custom properties in HubSpot for enriched fields:

// HubSpot API: Create Custom Properties
// Run this once to set up your custom fields

const axios = require('axios');

const customProperties = [
  {
    name: "email_verification_status",
    label: "Email Verification Status",
    type: "enumeration",
    fieldType: "select",
    groupName: "contactinformation",
    options: [
      { label: "Valid", value: "valid" },
      { label: "Invalid", value: "invalid" },
      { label: "Catch-All", value: "catch_all" },
      { label: "Unknown", value: "unknown" }
    ]
  },
  {
    name: "linkedin_profile_url",
    label: "LinkedIn Profile URL",
    type: "string",
    fieldType: "text",
    groupName: "contactinformation"
  },
  {
    name: "company_technologies",
    label: "Company Technologies",
    type: "string",
    fieldType: "textarea",
    groupName: "companyinformation"
  },
  {
    name: "company_employee_count_enriched",
    label: "Employee Count (Enriched)",
    type: "number",
    fieldType: "number",
    groupName: "companyinformation"
  },
  {
    name: "last_enriched_date",
    label: "Last Enriched Date",
    type: "datetime",
    fieldType: "date",
    groupName: "contactinformation"
  }
];

async function createHubSpotProperties(hubspotToken) {
  for (const prop of customProperties) {
    try {
      const response = await axios.post(
        'https://api.hubapi.com/crm/v3/properties/contacts',
        prop,
        {
          headers: {
            'Authorization': `Bearer ${hubspotToken}`,
            'Content-Type': 'application/json'
          }
        }
      );
      console.log(`✅ Created property: ${prop.name}`);
    } catch (error) {
      if (error.response?.status === 409) {
        console.log(`⏭️  Property already exists: ${prop.name}`);
      } else {
        console.error(`❌ Failed to create ${prop.name}:`, error.response?.data);
      }
    }
  }
}

// Run once
createHubSpotProperties(process.env.HUBSPOT_TOKEN);

🎯 Pro Tip: Name custom properties with _enriched suffix to distinguish from native HubSpot fields (e.g., company_employee_count_enriched vs HubSpot’s native numberofemployees).

4.2 Configure Clay Write-Back

Option A: UI-Based Write-Back (No Code)

In Clay table:

  1. Click “Integrations”“Write to HubSpot”

  2. Select write type: “Update Existing Contacts”

  3. Match on: “HubSpot Contact ID” (use {{hubspot_id}} column)

  4. Map fields:

    Clay Column → HubSpot Property
    ─────────────────────────────────
    email_verified → email_verification_status
    person_linkedin_url → linkedin_profile_url
    company_employee_count → company_employee_count_enriched
    company_industry → industry (native field)
    company_technologies → company_technologies
    [current_date] → last_enriched_date
  5. Set update mode: “Only update if empty” or “Always update”

  6. Enable “Skip rows with errors”

  7. Click “Write to HubSpot” → Wait for completion (5-30 sec)

Result: You’ll see a summary showing:

  • ✅ 47 contacts updated
  • ⚠️ 2 contacts skipped (missing HubSpot ID)
  • ❌ 1 contact failed (invalid property value)

Option B: API-Based Write-Back (Full Control)

// TypeScript: Write enriched data back to HubSpot
// Includes error handling, retry logic, and rate limiting

import axios from 'axios';
import pLimit from 'p-limit';

interface EnrichedContact {
  hubspot_id: string;
  email_verified: string;
  person_linkedin_url?: string;
  company_employee_count?: number;
  company_industry?: string;
  company_technologies?: string[];
}

const writeToHubSpot = async (
  enrichedContacts: EnrichedContact[],
  hubspotToken: string
) => {
  // Rate limiting: Max 10 concurrent requests to avoid 429 errors
  const limit = pLimit(10);

  const updatePromises = enrichedContacts.map(contact =>
    limit(() => updateContact(contact, hubspotToken))
  );

  const results = await Promise.allSettled(updatePromises);

  const summary = {
    successful: results.filter(r => r.status === 'fulfilled').length,
    failed: results.filter(r => r.status === 'rejected').length,
    errors: results
      .filter(r => r.status === 'rejected')
      .map(r => (r as PromiseRejectedResult).reason)
  };

  console.log('📊 Write-back summary:', summary);
  return summary;
};

const updateContact = async (
  contact: EnrichedContact,
  hubspotToken: string,
  retries = 3
): Promise<void> => {
  const url = `https://api.hubapi.com/crm/v3/objects/contacts/${contact.hubspot_id}`;

  const payload = {
    properties: {
      email_verification_status: contact.email_verified,
      linkedin_profile_url: contact.person_linkedin_url || '',
      company_employee_count_enriched: contact.company_employee_count || null,
      industry: contact.company_industry || '',
      company_technologies: contact.company_technologies?.join(', ') || '',
      last_enriched_date: new Date().toISOString()
    }
  };

  try {
    await axios.patch(url, payload, {
      headers: {
        'Authorization': `Bearer ${hubspotToken}`,
        'Content-Type': 'application/json'
      }
    });

    console.log(`✅ Updated contact: ${contact.hubspot_id}`);
  } catch (error) {
    if (axios.isAxiosError(error)) {
      // Handle rate limiting with exponential backoff
      if (error.response?.status === 429 && retries > 0) {
        const retryAfter = parseInt(error.response.headers['retry-after'] || '2');
        console.log(`⏳ Rate limited. Retrying in ${retryAfter}s...`);
        await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
        return updateContact(contact, hubspotToken, retries - 1);
      }

      // Handle validation errors
      if (error.response?.status === 400) {
        console.error(`❌ Validation error for ${contact.hubspot_id}:`,
          error.response.data.message);
        throw new Error(`Invalid data: ${error.response.data.message}`);
      }

      // Handle missing contact
      if (error.response?.status === 404) {
        console.error(`❌ Contact not found: ${contact.hubspot_id}`);
        throw new Error(`Contact ${contact.hubspot_id} does not exist`);
      }
    }

    console.error(`❌ Failed to update ${contact.hubspot_id}:`, error);
    throw error;
  }
};

// Usage example
const enrichedData: EnrichedContact[] = [
  {
    hubspot_id: '12345',
    email_verified: 'valid',
    person_linkedin_url: 'https://linkedin.com/in/johndoe',
    company_employee_count: 250,
    company_industry: 'Software',
    company_technologies: ['Salesforce', 'HubSpot', 'Slack']
  },
  // ... more contacts
];

writeToHubSpot(enrichedData, process.env.HUBSPOT_TOKEN!)
  .then(summary => console.log('🎉 Write-back complete:', summary))
  .catch(err => console.error('💥 Write-back failed:', err));

Error Handling Coverage:

  • ✅ Rate limiting (429) with exponential backoff
  • ✅ Validation errors (400) with detailed logging
  • ✅ Missing contacts (404) gracefully skipped
  • ✅ Network failures with retry logic
  • ✅ Concurrent request limiting (prevents overwhelming HubSpot API)

4.3 Field Mapping Decision Matrix

Not sure whether to update or skip? Use this matrix:

HubSpot Field StatusClay Has DataActionReason
EmptyYesUpdateFill in missing data
EmptyNoSkipNo value to add
Has ValueYes, more recentUpdateFresher data wins
Has ValueYes, olderSkipDon’t overwrite good data
Has Value (manual)Yes (enriched)SkipRespect manual entry
Has ValueYes, conflictingCreate custom fieldKeep both sources

Implementation in Clay:

In write-back settings:

  • ☑ Update only if HubSpot field is empty
  • ☐ Always update (overwrite existing)
  • ☑ Append to existing values (for multi-select fields)
  • ☑ Skip if last modified by user (respect manual edits)

Step 5: Automate Real-Time Enrichment (7 min)

5.1 Set Up HubSpot Webhook → Clay Trigger

This is where the magic happens: New contact created in HubSpot → Automatically enriched within 30 seconds.

Configure HubSpot Workflow

  1. In HubSpot, go to AutomationWorkflows
  2. Click “Create workflow”“Contact-based”
  3. Name: Clay Auto-Enrichment

Enrollment Trigger:

"Contact is created"

Optional filters:
- Email is known
- Contact owner is any of [your team]
- Lead status is any of [New, Working]

Action:

"Send a webhook"

Webhook URL: https://api.clay.com/v1/webhooks/inbound/YOUR_WEBHOOK_ID

Method: POST

Request body:
{
  "contact_id": "{{contact.id}}",
  "email": "{{contact.email}}",
  "firstname": "{{contact.firstname}}",
  "lastname": "{{contact.lastname}}",
  "company": "{{contact.company}}",
  "jobtitle": "{{contact.jobtitle}}",
  "created_at": "{{contact.createdate}}"
}
  1. Turn on workflow

Get Your Clay Webhook URL

In Clay:

  1. Go to AutomationsWebhooks
  2. Click “Create Webhook Receiver”
  3. Name: HubSpot New Contact
  4. Copy webhook URL: https://api.clay.com/v1/webhooks/inbound/wh_abc123xyz
  5. Test with sample payload (use HubSpot’s “Test” button)

5.2 Configure Clay Automation

In Clay Automations:

  1. Click “New Automation”

  2. Trigger: “Webhook Received” (select your HubSpot webhook)

  3. Action: “Add Row to Table”

    • Table: Your enrichment table from Step 3
    • Map webhook fields to table columns:
      • {{webhook.contact_id}}hubspot_id
      • {{webhook.email}}email
      • {{webhook.firstname}}first_name
      • {{webhook.lastname}}last_name
      • {{webhook.company}}company
      • {{webhook.jobtitle}}job_title
  4. Action: “Run Enrichments”

    • Wait for: All enrichment columns complete
    • Timeout: 60 seconds
  5. Action: “Write to HubSpot”

    • Match on: hubspot_id
    • Update fields: (your mapping from Step 4.2)
  6. Optional Action: “Send Slack Notification”

    • Channel: #sales-ops
    • Message: Enriched {{email}}: {{company_employee_count}} employees at {{company_industry}} company
  7. Turn on automation

Test the Flow:

# Create a test contact in HubSpot
curl -X POST https://api.hubapi.com/crm/v3/objects/contacts \
  -H "Authorization: Bearer YOUR_HUBSPOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "properties": {
      "email": "test@example.com",
      "firstname": "Test",
      "lastname": "Contact",
      "company": "Example Corp"
    }
  }'

# Expected flow:
# 1. Contact created in HubSpot → triggers workflow
# 2. Webhook fires to Clay → adds row to table
# 3. Clay enrichments run → 5-15 seconds
# 4. Data written back to HubSpot → 2 seconds
# 5. Total time: 7-17 seconds from creation to enriched

5.3 Alternative Trigger: Scheduled Sync

If you don’t want real-time (to batch API calls and reduce costs):

In Clay:

  1. Create Automation“Scheduled Run”
  2. Schedule: Every 6 hours (or daily at 9 AM)
  3. Action: “Import from HubSpot”
    • Filter: Created in last 6 hours
    • Limit: 1000 contacts
  4. Action: “Run Enrichments” (same as above)
  5. Action: “Write to HubSpot” (same as above)

Cost Comparison:

MethodLatencyAPI CallsCost per 1000 contacts
Real-time webhook7-17 sec1000$0.05 (API fees)
Scheduled (6hr)Up to 6 hours167 (batched)$0.01 (API fees)

Recommendation: Use real-time for high-value inbound leads, scheduled for bulk imports.

Step 6: Advanced Patterns & Optimization

6.1 Bidirectional Sync (HubSpot Changes → Clay)

Want Clay to re-enrich when data changes in HubSpot?

HubSpot Workflow: “Contact Property Changed”

Trigger:
  Job Title is updated OR
  Company is updated

Action: Send webhook to Clay
  URL: https://api.clay.com/v1/webhooks/inbound/wh_update_xyz
  Body: {
    "contact_id": "{{contact.id}}",
    "updated_field": "job_title",
    "new_value": "{{contact.jobtitle}}"
  }

Clay Automation: “Re-Enrichment Trigger”

Trigger: Webhook received

Condition: Check if last enrichment > 30 days ago

Action: Find existing row in table (match on contact_id)

Action: Re-run enrichments (only changed fields)

Action: Write back to HubSpot

Use Case: Sales rep manually updates job title → Clay re-enriches company data based on new title.

6.2 Enrichment Quality Scoring

Add a “data quality score” to prioritize follow-up:

In Clay, add Formula Column: enrichment_quality_score

Formula:

(
  ({{email_verified}} == "valid" ? 25 : 0) +
  ({{person_linkedin_url}} != null ? 20 : 0) +
  ({{company_employee_count}} > 0 ? 15 : 0) +
  ({{company_industry}} != null ? 15 : 0) +
  ({{company_technologies}}.length > 0 ? 25 : 0)
) / 100

Result: 0.0 to 1.0 score

Write back to HubSpot custom property: enrichment_quality_score

Sales Playbook:

  • Score > 0.8 → “Hot Lead” (all data enriched)
  • Score 0.5-0.8 → “Warm Lead” (partial enrichment)
  • Score < 0.5 → “Cold Lead” (needs manual research)

6.3 AI-Powered Enrichment with GPT-4

Clay lets you call OpenAI APIs for custom enrichment:

Use Case: Generate personalized outreach based on LinkedIn profile

Clay Column: ai_personalization Type: AI Formula (GPT-4)

Prompt:

Based on this LinkedIn profile data:
- Job Title: {{person_job_title}}
- Company: {{company}}
- Industry: {{company_industry}}
- Technologies: {{company_technologies}}

Generate a 2-sentence personalized cold email opener that:
1. References their specific role and company
2. Mentions a pain point relevant to their tech stack
3. Is professional and conversational

Output format: Plain text, no greeting or signature.

Example Output:

“I noticed your team at Acme Corp is using Salesforce and HubSpot together—managing data sync between those two can be a huge time sink. We’ve helped similar SaaS companies automate that workflow and cut admin time by 15 hours/week.”

Cost: $0.002 per generation (GPT-4-mini) Write back to: hubspot_custom_fieldai_outreach_snippet

Step 7: Troubleshooting Guide

Issue 1: Enrichment Not Running

Symptoms:

  • Contacts imported but enrichment columns empty
  • “Pending” status stuck for >5 minutes
  • No errors shown

Diagnosis:

# Check Clay credits
curl -X GET https://api.clay.com/v1/credits \
  -H "Authorization: Bearer YOUR_CLAY_API_KEY"

# Expected response:
{
  "available_credits": 487,
  "monthly_limit": 1000,
  "reset_date": "2025-02-01T00:00:00Z"
}

Solutions:

  1. Insufficient credits: Top up in Clay billing
  2. Invalid email format: Add validation formula:
    {{email}} REGEX_MATCH "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
  3. Enrichment provider down: Check Clay status page or swap waterfall order
  4. Rate limiting: Add 100ms delay between rows in table settings

Issue 2: HubSpot Write-Back Fails

Symptoms:

  • “403 Forbidden” errors
  • “Property does not exist” errors
  • “Invalid property value” errors

Diagnosis:

// Test HubSpot API access
const testHubSpotAccess = async (token, contactId) => {
  try {
    // Test read
    const readResponse = await axios.get(
      `https://api.hubapi.com/crm/v3/objects/contacts/${contactId}`,
      { headers: { 'Authorization': `Bearer ${token}` } }
    );
    console.log('✅ Read access: OK');

    // Test write
    const writeResponse = await axios.patch(
      `https://api.hubapi.com/crm/v3/objects/contacts/${contactId}`,
      { properties: { lastname: readResponse.data.properties.lastname } },
      { headers: { 'Authorization': `Bearer ${token}` } }
    );
    console.log('✅ Write access: OK');

  } catch (error) {
    if (error.response?.status === 403) {
      console.error('❌ Missing API scopes. Required: crm.objects.contacts.write');
    } else if (error.response?.status === 404) {
      console.error('❌ Contact not found. Check contact ID.');
    } else {
      console.error('❌ Error:', error.response?.data);
    }
  }
};

Solutions:

  1. Missing scopes: Regenerate HubSpot token with correct scopes (see Step 1.2)
  2. Property doesn’t exist: Create custom property first (see Step 4.1)
  3. Invalid value format: Check field type matches (e.g., number vs string)
  4. Contact ID mismatch: Verify hubspot_id column has correct IDs

Issue 3: Duplicate Contacts Created

Symptoms:

  • Same email appears 2-3x in HubSpot
  • Enrichment runs multiple times for same contact

Root Cause: Webhook fires multiple times or Clay automation doesn’t check for existing records.

Solution:

// Add deduplication check in Clay automation

async function addContactWithDedupe(email: string, clayTableId: string) {
  // Check if email already exists in Clay table
  const existingRows = await clay.tables.search(clayTableId, {
    filters: [{ field: 'email', operator: 'equals', value: email }]
  });

  if (existingRows.length > 0) {
    console.log(`⏭️  Contact ${email} already exists. Skipping.`);
    return { skipped: true, reason: 'duplicate' };
  }

  // Add new row if not exists
  return await clay.tables.addRow(clayTableId, {
    email,
    // ... other fields
  });
}

Or in Clay UI: SettingsAutomations → “Check for duplicates before adding” ☑

Issue 4: Rate Limiting (429 Errors)

Symptoms:

  • Sporadic “Too Many Requests” errors
  • Write-back succeeds for first 50 contacts, then fails
  • HubSpot API returns retry-after header

HubSpot API Limits:

  • 100 requests per 10 seconds (per token)
  • 4 concurrent requests max
  • 500,000 requests per day

Solution:

// Implement rate limiter with exponential backoff

import Bottleneck from 'bottleneck';

const hubspotLimiter = new Bottleneck({
  maxConcurrent: 4,        // Max 4 simultaneous requests
  minTime: 100,            // Min 100ms between requests
  reservoir: 100,          // 100 requests
  reservoirRefreshAmount: 100,
  reservoirRefreshInterval: 10 * 1000  // Per 10 seconds
});

// Wrap HubSpot API calls
const updateContactWithRateLimit = hubspotLimiter.wrap(
  async (contactId: string, properties: any, token: string) => {
    return axios.patch(
      `https://api.hubapi.com/crm/v3/objects/contacts/${contactId}`,
      { properties },
      { headers: { 'Authorization': `Bearer ${token}` } }
    );
  }
);

// Usage
await updateContactWithRateLimit(contactId, enrichedData, hubspotToken);
// Automatically respects rate limits

Or use Clay’s built-in rate limiting: SettingsIntegrationsHubSpot → “Rate limit: 10 requests/second” (recommended)

Issue 5: Enrichment Match Rate Lower Than Expected

Symptoms:

  • Getting 40-50% match rate instead of 75-85%
  • Lots of empty enrichment fields
  • High cost per successful enrichment

Diagnosis:

// Analyze match rate by waterfall source

const analyzeWaterfallPerformance = (enrichmentResults) => {
  const stats = {
    total: enrichmentResults.length,
    by_source: {}
  };

  enrichmentResults.forEach(result => {
    const source = result.successful_source || 'none';
    stats.by_source[source] = (stats.by_source[source] || 0) + 1;
  });

  console.log('Match Rate Analysis:');
  Object.entries(stats.by_source).forEach(([source, count]) => {
    const percentage = ((count / stats.total) * 100).toFixed(1);
    console.log(`${source}: ${count} (${percentage}%)`);
  });

  return stats;
};

// Example output:
// apollo: 234 (46.8%)
// peopledatalabs: 89 (17.8%)
// rocketreach: 45 (9.0%)
// none: 132 (26.4%)
// Total match rate: 73.6%

Solutions:

  1. Improve input data quality:

    • Ensure email is valid (add validation column)
    • Provide full name (not just email)
    • Include company domain when available
  2. Optimize waterfall order based on your vertical:

    • Tech companies: Apollo → PeopleDataLabs → Clearbit
    • Healthcare: PeopleDataLabs → ZoomInfo → Apollo
    • Finance: Clearbit → PeopleDataLabs → Apollo
  3. Add more sources: Include 4-5 providers instead of 2-3

  4. Use AI fallback: If all enrichments fail, use GPT-4 to research from LinkedIn

  5. Check data freshness: Some providers have better data for recent contacts

Step 8: ROI Calculator & Expected Results

8.1 Cost Breakdown (Per 1,000 Contacts)

Cost CategoryAmountNotes
Clay Credits$12-25Depends on waterfall match rate
Email Verification$1-2$0.001-0.002 per verification
Person Enrichment$8-15LinkedIn, job title, etc.
Company Enrichment$3-8Size, industry, revenue
Tech Stack$15-30Optional, only for ICP matches
HubSpot API$0Included in HubSpot subscription
Total per 1k contacts$24-50Lower with cache hits

Cost Optimization:

  • With 60% cache hit rate: $10-20 per 1k contacts
  • With conditional enrichment: $8-15 per 1k contacts

8.2 Time Savings Calculation

Manual Research (Before Clay):

  • Time per contact: 12-20 minutes
  • 100 contacts = 20-33 hours = $1,000-1,650 at $50/hr

Automated Enrichment (With Clay):

  • Time per contact: 15 seconds (including review)
  • 100 contacts = 25 minutes = $21 at $50/hr

Net Savings per 100 Contacts:

  • Time saved: 19-32 hours
  • Cost saved: $979-1,629
  • Enrichment cost: $2.40-5.00
  • Total ROI: 19,580% - 65,160%

8.3 Match Rate Benchmarks

Expected enrichment success rates by field:

Data PointMatch RateNotes
Email Validation95-98%Highest accuracy
Company Domain85-92%From email domain
Company Size78-85%B2B companies only
Company Industry80-88%
LinkedIn Profile65-78%Higher for tech/sales roles
Job Title (Standardized)70-80%
Tech Stack55-68%Only for SMB+ companies
Funding Data40-55%VC-backed companies only
Phone Number15-30%Privacy regulations limit

Industry Benchmarks:

  • Tech/SaaS: 82% overall match rate
  • Healthcare: 74% overall match rate
  • Manufacturing: 68% overall match rate
  • Retail/CPG: 63% overall match rate

8.4 Processing Speed

Batch SizeProcessing TimeNotes
10 contacts15-30 secondsIndividual waterfall runs
100 contacts2-4 minutesParallel processing
1,000 contacts15-25 minutesRate limiting applies
10,000 contacts2.5-4 hoursScheduled batch recommended

Optimization: Enable parallel enrichment for 3-5x speed improvement (may increase cost by 10-15%).

Step 9: Maintenance & Monitoring

9.1 Weekly Health Checks

// Automated health check script (run weekly via cron)

const runHealthCheck = async () => {
  console.log('🏥 Running Clay HubSpot Integration Health Check...\n');

  // 1. Check API connectivity
  const clayConnected = await testClayAPI();
  const hubspotConnected = await testHubSpotAPI();

  // 2. Check credit balance
  const credits = await clay.credits.get();
  if (credits.available < 100) {
    alert('⚠️ LOW CREDITS: Only ' + credits.available + ' remaining');
  }

  // 3. Check recent enrichment quality
  const last7Days = await clay.enrichments.getStats({
    from: Date.now() - 7 * 24 * 60 * 60 * 1000,
    to: Date.now()
  });

  console.log(`📊 7-Day Stats:`);
  console.log(`  Contacts enriched: ${last7Days.total_enriched}`);
  console.log(`  Match rate: ${last7Days.match_rate}%`);
  console.log(`  Avg cost: ${last7Days.avg_cost_per_contact}`);
  console.log(`  Failed enrichments: ${last7Days.failed_count}`);

  if (last7Days.match_rate < 70) {
    alert('⚠️ LOW MATCH RATE: Only ' + last7Days.match_rate + '%');
  }

  // 4. Check write-back success rate
  const writebacks = await clay.hubspot.getWriteStats({
    from: Date.now() - 7 * 24 * 60 * 60 * 1000
  });

  console.log(`\n📤 Write-back Stats:`);
  console.log(`  Total writes: ${writebacks.total}`);
  console.log(`  Successful: ${writebacks.successful} (${writebacks.success_rate}%)`);
  console.log(`  Failed: ${writebacks.failed}`);

  if (writebacks.success_rate < 95) {
    alert('⚠️ LOW WRITE SUCCESS: Only ' + writebacks.success_rate + '%');
  }

  console.log('\n✅ Health check complete');
};

// Schedule: Every Monday at 9 AM
schedule.scheduleJob('0 9 * * 1', runHealthCheck);

Create a HubSpot dashboard to track enrichment performance:

Key Metrics to Display:

  1. Contacts Enriched (Last 30 Days) - Line chart
  2. Enrichment Quality Score Distribution - Bar chart
  3. Cost per Enriched Contact - Single value + trend
  4. Match Rate by Data Type - Stacked bar chart
  5. Time from Contact Creation to Enrichment - Average value
  6. Failed Enrichments - Table with reasons
  7. Top Enrichment Sources - Pie chart

Alert Thresholds:

  • Match rate drops below 70%: Send Slack alert
  • Cost per contact exceeds $0.08: Send email alert
  • Failed enrichments exceed 10%: Send PagerDuty alert
  • Credit balance below 50: Send billing team alert

9.3 Monthly Optimization Review

Review Checklist:

  • Analyze waterfall performance by source
  • Adjust source order based on match rates
  • Review enrichment costs vs budget
  • Check for new Clay enrichment providers
  • Update field mappings for new HubSpot properties
  • Review and archive unused enrichment columns
  • Test enrichment quality with 10 sample contacts
  • Update documentation with any workflow changes
  • Verify API scopes haven’t changed
  • Check for Clay product updates/new features

Frequently Asked Questions (FAQ)

How much does Clay HubSpot integration cost?

Enrichment Costs:

  • Email verification: $0.001-0.002 per contact
  • Basic enrichment (person + company): $0.015-0.025 per contact
  • Full enrichment (including tech stack): $0.035-0.055 per contact

Clay Subscription:

  • Free tier: 50 credits/month (enough for ~50 contacts)
  • Growth: $149/month (1,500 credits = ~1,500 contacts)
  • Pro: $349/month (5,000 credits = ~5,000 contacts)

Total Cost Example:

  • 1,000 contacts/month with full enrichment: $50 enrichment + $149 subscription = $199/month
  • ROI vs manual research: $1,629 saved per 100 contacts

Can I sync Clay data to HubSpot in real-time?

Yes. Using HubSpot workflows + Clay webhooks, you can achieve sub-30-second enrichment:

  1. Contact created in HubSpot
  2. Workflow triggers webhook to Clay (1-2 seconds)
  3. Clay enriches contact (5-15 seconds)
  4. Data written back to HubSpot (2-3 seconds)
  5. Total: 8-20 seconds

For scheduled batch processing (lower cost), you can sync every 1-24 hours.

What’s the best enrichment waterfall order for Clay?

Depends on your industry:

Tech/SaaS Companies:

  1. Clay Cache (free)
  2. Apollo.io (best tech coverage)
  3. Clearbit (enterprise data)
  4. PeopleDataLabs (fallback)

Healthcare/Life Sciences:

  1. Clay Cache (free)
  2. ZoomInfo (best healthcare coverage)
  3. PeopleDataLabs
  4. Apollo.io

Financial Services:

  1. Clay Cache (free)
  2. Clearbit (strong finance data)
  3. PeopleDataLabs
  4. RocketReach

General Rule: Order by (match rate ÷ cost) ratio for your ICP.

How do I prevent duplicate contacts in HubSpot?

3-Layer Deduplication Strategy:

  1. In Clay (before enrichment):

    • Add “Dedupe” column matching on email
    • Keep most recent record only
  2. In HubSpot (native):

    • Enable automatic deduplication (Settings → Objects → Contacts)
    • Set match criteria: Email + Company Domain
  3. In Integration (write-back logic):

    • Use “Update existing” instead of “Create new”
    • Match on HubSpot Contact ID when available
    • Check for existing email before creating

Can Clay enrich contacts from other sources besides HubSpot?

Yes! Clay integrates with 50+ data sources:

CRMs:

  • HubSpot (this guide)
  • Salesforce
  • Pipedrive
  • Copper

Email/Outreach:

  • Gmail (via API)
  • Outlook
  • Apollo
  • Outreach.io

Spreadsheets:

  • Google Sheets
  • Airtable
  • Excel (via upload)

Custom Sources:

  • CSV upload
  • API webhook
  • Zapier integration
  • Make (Integromat)

Enrichment Providers:

  • Apollo, Clearbit, ZoomInfo, PeopleDataLabs, RocketReach, Hunter, etc.

How accurate is Clay’s enrichment data?

Accuracy Benchmarks (from Clay):

  • Email verification: 98% accuracy
  • Company data (size, industry): 90-95% accuracy
  • LinkedIn profiles: 85-90% accuracy (when found)
  • Tech stack: 80-85% accuracy for companies with 10+ employees

Data Freshness:

  • Person data: Updated every 30-90 days
  • Company data: Updated every 14-30 days
  • Tech stack: Updated every 60-90 days

Validation Methods:

  • Cross-reference 3+ sources in waterfall
  • Add AI validation step (GPT-4 checks for inconsistencies)
  • Manual QA on sample of 50 contacts monthly

What happens if Clay enrichment fails?

Failure Handling Options:

  1. Waterfall continues: Tries next source automatically
  2. Fallback value: Set default (null, “unknown”, etc.)
  3. Manual review: Flag for sales ops review
  4. Retry logic: Attempt again in 24 hours (scheduled job)

Common Failure Reasons:

  • Invalid/non-existent email (30% of failures)
  • No data available (company too small, too new) (40%)
  • API provider downtime (5%)
  • Rate limiting (15%)
  • Data format mismatch (10%)

Best Practice: Set up Slack alerts for failed enrichments >10% to catch systematic issues early.

Can I use Clay for companies outside the US?

Yes, with varying coverage by region:

RegionCoverageBest Providers
United States85-92%Apollo, Clearbit, ZoomInfo
Canada75-82%PeopleDataLabs, Apollo
UK/Western Europe70-80%PeopleDataLabs, Clearbit
APAC55-70%PeopleDataLabs, LinkedIn
Latin America50-65%PeopleDataLabs
Middle East/Africa40-55%LinkedIn scraping + AI

Pro Tip for International Markets:

  • Use local business registrar APIs for B2B data
  • Combine local language + English search
  • Use LinkedIn as primary source (70-80%+ coverage in tech sectors)
  • Custom enrichment functions for multi-language processing

How do I handle GDPR/privacy compliance?

Clay + HubSpot Compliance:

  1. Data Minimization: Only enrich fields you need

  2. Consent Tracking:

    • Add HubSpot property: enrichment_consent
    • Only enrich if consent = true
  3. Right to Deletion:

    • When contact deleted in HubSpot, delete from Clay
    • Set up HubSpot workflow → Clay webhook for deletions
  4. Data Retention:

    • Auto-archive enrichments older than 2 years
    • Store audit logs of all enrichments

GDPR-Compliant Waterfall:

  1. HubSpot existing data (already consented)
  2. Public sources only (LinkedIn, company websites)
  3. Consent-verified enrichment providers (Clearbit, ZoomInfo)
  4. Skip: Purchasing email lists or non-consented sources

Documentation: Maintain “Data Processing Agreement” with Clay (available in Clay’s security center).

Can I enrich contacts retroactively (already in HubSpot)?

Yes! Two methods:

Method 1: Bulk Import to Clay

  1. Export contacts from HubSpot (Contacts → Export)
  2. Import CSV to Clay
  3. Run enrichments on entire table
  4. Write back to HubSpot (match on Contact ID)

Method 2: HubSpot List-Based Trigger

  1. Create HubSpot list: “Contacts Missing Enrichment Data”
    • Filters:
      • LinkedIn Profile URL is unknown
      • OR Company Employee Count is unknown
  2. HubSpot Workflow: “Enroll in Clay Enrichment”
    • Trigger: Contact is added to list
    • Action: Send webhook to Clay
  3. Clay processes and writes back
  4. Contact automatically removed from list (criteria no longer met)

Processing Time:

  • 1,000 contacts: 15-25 minutes
  • 10,000 contacts: 2-4 hours
  • 100,000 contacts: 24-48 hours (batch processing)

What’s the difference between Clay and Clearbit?

FeatureClayClearbit
Core FunctionEnrichment orchestration (uses multiple sources)Single enrichment provider
Data Sources50+ providers (including Clearbit)Clearbit database only
Cost OptimizationWaterfall logic (uses cheapest source first)Fixed per-lookup cost
Match Rate75-85% (combined sources)60-70%
Cost per Contact$0.01-0.05$0.50-1.50
Best ForHigh volume, cost-sensitiveEnterprise, brand data
HubSpot IntegrationNative 2-way syncNative 2-way sync

Recommendation: Use Clay with Clearbit in the waterfall for best of both worlds - Clay’s orchestration + Clearbit’s premium data when needed.

Essential Reading

Integration Guides

Community & Support

Next Steps

Immediate Actions (This Week)

  • ✅ Complete HubSpot API setup (Step 1)
  • ✅ Connect Clay to HubSpot (Step 2)
  • ✅ Test with 10-50 contacts (Steps 3-4)
  • ✅ Validate enrichment quality manually
  • ✅ Set up basic webhook automation (Step 5)

Short-Term Optimization (Next 2 Weeks)

  • 📊 Analyze match rates by enrichment source
  • 💰 Optimize waterfall order for your ICP
  • 🎯 Create enrichment quality score field
  • 📧 Set up monitoring alerts (Slack/email)
  • 📈 Build HubSpot dashboard for tracking

Long-Term Scaling (Next Month)

  • 🤖 Implement AI-powered personalization (Step 6.3)
  • 🔄 Enable bidirectional sync (Step 6.1)
  • 🧹 Set up automated deduplication
  • 📊 Create ROI report for stakeholders
  • 🚀 Scale to full contact database

🎉 Congratulations! You now have a production-ready Clay HubSpot integration that will save your team 15+ hours per week and enrich contacts at $0.01-0.05 each instead of $50/hour manual research.

Final Pro Tip: Start small with 50-100 contacts to validate your waterfall configuration, then scale gradually. Monitor your match rates and costs closely in the first two weeks, and adjust your waterfall order based on actual performance data. The most successful implementations optimize continuously rather than “set and forget.”

Need Implementation Help?

Our team can build this integration for you in 48 hours. From strategy to deployment.

Get Started

Need Implementation Help?

Our team can build this integration for you in 48 hours. From strategy to deployment.

Get Started