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.
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
| Requirement | Free 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 | ❌ No | Need to create private apps |
| Basic API Knowledge | ⚠️ Helpful | Follow 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
- Log into HubSpot → Settings (⚙️ icon top right)
- Navigate to Integrations → Private Apps
- Click Create a private app
- 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
- Click Create app
- Copy the access token immediately (shows once)
- Store securely (use 1Password, never commit to git)
- 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)
2.1 UI-Based Connection (Recommended)
// 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 wrong403 Forbidden→ HubSpot token missing required scopes422 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)
- In Clay, click “Import Data” → “HubSpot”
- Select object: “Contacts”
- Choose import fields:
- ☑ Email (required for enrichment)
- ☑ First Name
- ☑ Last Name
- ☑ Company Name
- ☑ Job Title
- ☑ HubSpot Contact ID (required for write-back)
- ☑ Create Date
- Add filter: “Create Date is after [7 days ago]”
- Set limit: 50 (start small for testing)
- 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:
- Add “Formula” column:
should_enrich_techstack - Formula:
{{company_employee_count}} > 50 AND ({{company_industry}} CONTAINS "Software" OR {{company_industry}} CONTAINS "Technology") AND {{tech_stack_last_enriched}} > 30 days ago - 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:
-
Click “Integrations” → “Write to HubSpot”
-
Select write type: “Update Existing Contacts”
-
Match on: “HubSpot Contact ID” (use
{{hubspot_id}}column) -
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 -
Set update mode: “Only update if empty” or “Always update”
-
Enable “Skip rows with errors”
-
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 Status | Clay Has Data | Action | Reason |
|---|---|---|---|
| Empty | Yes | Update | Fill in missing data |
| Empty | No | Skip | No value to add |
| Has Value | Yes, more recent | Update | Fresher data wins |
| Has Value | Yes, older | Skip | Don’t overwrite good data |
| Has Value (manual) | Yes (enriched) | Skip | Respect manual entry |
| Has Value | Yes, conflicting | Create custom field | Keep 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
- In HubSpot, go to Automation → Workflows
- Click “Create workflow” → “Contact-based”
- 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}}"
}
- Turn on workflow
Get Your Clay Webhook URL
In Clay:
- Go to Automations → Webhooks
- Click “Create Webhook Receiver”
- Name:
HubSpot New Contact - Copy webhook URL:
https://api.clay.com/v1/webhooks/inbound/wh_abc123xyz - Test with sample payload (use HubSpot’s “Test” button)
5.2 Configure Clay Automation
In Clay Automations:
-
Click “New Automation”
-
Trigger: “Webhook Received” (select your HubSpot webhook)
-
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
-
Action: “Run Enrichments”
- Wait for: All enrichment columns complete
- Timeout: 60 seconds
-
Action: “Write to HubSpot”
- Match on:
hubspot_id - Update fields: (your mapping from Step 4.2)
- Match on:
-
Optional Action: “Send Slack Notification”
- Channel:
#sales-ops - Message:
Enriched {{email}}: {{company_employee_count}} employees at {{company_industry}} company
- Channel:
-
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:
- Create Automation → “Scheduled Run”
- Schedule: Every 6 hours (or daily at 9 AM)
- Action: “Import from HubSpot”
- Filter: Created in last 6 hours
- Limit: 1000 contacts
- Action: “Run Enrichments” (same as above)
- Action: “Write to HubSpot” (same as above)
Cost Comparison:
| Method | Latency | API Calls | Cost per 1000 contacts |
|---|---|---|---|
| Real-time webhook | 7-17 sec | 1000 | $0.05 (API fees) |
| Scheduled (6hr) | Up to 6 hours | 167 (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_field → ai_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:
- Insufficient credits: Top up in Clay billing
- Invalid email format: Add validation formula:
{{email}} REGEX_MATCH "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" - Enrichment provider down: Check Clay status page or swap waterfall order
- 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:
- Missing scopes: Regenerate HubSpot token with correct scopes (see Step 1.2)
- Property doesn’t exist: Create custom property first (see Step 4.1)
- Invalid value format: Check field type matches (e.g., number vs string)
- Contact ID mismatch: Verify
hubspot_idcolumn 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: Settings → Automations → “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-afterheader
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: Settings → Integrations → HubSpot → “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:
-
Improve input data quality:
- Ensure email is valid (add validation column)
- Provide full name (not just email)
- Include company domain when available
-
Optimize waterfall order based on your vertical:
- Tech companies: Apollo → PeopleDataLabs → Clearbit
- Healthcare: PeopleDataLabs → ZoomInfo → Apollo
- Finance: Clearbit → PeopleDataLabs → Apollo
-
Add more sources: Include 4-5 providers instead of 2-3
-
Use AI fallback: If all enrichments fail, use GPT-4 to research from LinkedIn
-
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 Category | Amount | Notes |
|---|---|---|
| Clay Credits | $12-25 | Depends on waterfall match rate |
| Email Verification | $1-2 | $0.001-0.002 per verification |
| Person Enrichment | $8-15 | LinkedIn, job title, etc. |
| Company Enrichment | $3-8 | Size, industry, revenue |
| Tech Stack | $15-30 | Optional, only for ICP matches |
| HubSpot API | $0 | Included in HubSpot subscription |
| Total per 1k contacts | $24-50 | Lower 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 Point | Match Rate | Notes |
|---|---|---|
| Email Validation | 95-98% | Highest accuracy |
| Company Domain | 85-92% | From email domain |
| Company Size | 78-85% | B2B companies only |
| Company Industry | 80-88% | |
| LinkedIn Profile | 65-78% | Higher for tech/sales roles |
| Job Title (Standardized) | 70-80% | |
| Tech Stack | 55-68% | Only for SMB+ companies |
| Funding Data | 40-55% | VC-backed companies only |
| Phone Number | 15-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 Size | Processing Time | Notes |
|---|---|---|
| 10 contacts | 15-30 seconds | Individual waterfall runs |
| 100 contacts | 2-4 minutes | Parallel processing |
| 1,000 contacts | 15-25 minutes | Rate limiting applies |
| 10,000 contacts | 2.5-4 hours | Scheduled 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);
9.2 Monitoring Dashboard (Recommended Metrics)
Create a HubSpot dashboard to track enrichment performance:
Key Metrics to Display:
- Contacts Enriched (Last 30 Days) - Line chart
- Enrichment Quality Score Distribution - Bar chart
- Cost per Enriched Contact - Single value + trend
- Match Rate by Data Type - Stacked bar chart
- Time from Contact Creation to Enrichment - Average value
- Failed Enrichments - Table with reasons
- 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:
- Contact created in HubSpot
- Workflow triggers webhook to Clay (1-2 seconds)
- Clay enriches contact (5-15 seconds)
- Data written back to HubSpot (2-3 seconds)
- 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:
- Clay Cache (free)
- Apollo.io (best tech coverage)
- Clearbit (enterprise data)
- PeopleDataLabs (fallback)
Healthcare/Life Sciences:
- Clay Cache (free)
- ZoomInfo (best healthcare coverage)
- PeopleDataLabs
- Apollo.io
Financial Services:
- Clay Cache (free)
- Clearbit (strong finance data)
- PeopleDataLabs
- RocketReach
General Rule: Order by (match rate ÷ cost) ratio for your ICP.
How do I prevent duplicate contacts in HubSpot?
3-Layer Deduplication Strategy:
-
In Clay (before enrichment):
- Add “Dedupe” column matching on email
- Keep most recent record only
-
In HubSpot (native):
- Enable automatic deduplication (Settings → Objects → Contacts)
- Set match criteria: Email + Company Domain
-
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:
- Waterfall continues: Tries next source automatically
- Fallback value: Set default (null, “unknown”, etc.)
- Manual review: Flag for sales ops review
- 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:
| Region | Coverage | Best Providers |
|---|---|---|
| United States | 85-92% | Apollo, Clearbit, ZoomInfo |
| Canada | 75-82% | PeopleDataLabs, Apollo |
| UK/Western Europe | 70-80% | PeopleDataLabs, Clearbit |
| APAC | 55-70% | PeopleDataLabs, LinkedIn |
| Latin America | 50-65% | PeopleDataLabs |
| Middle East/Africa | 40-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:
-
Data Minimization: Only enrich fields you need
-
Consent Tracking:
- Add HubSpot property:
enrichment_consent - Only enrich if consent = true
- Add HubSpot property:
-
Right to Deletion:
- When contact deleted in HubSpot, delete from Clay
- Set up HubSpot workflow → Clay webhook for deletions
-
Data Retention:
- Auto-archive enrichments older than 2 years
- Store audit logs of all enrichments
GDPR-Compliant Waterfall:
- HubSpot existing data (already consented)
- Public sources only (LinkedIn, company websites)
- Consent-verified enrichment providers (Clearbit, ZoomInfo)
- 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
- Export contacts from HubSpot (Contacts → Export)
- Import CSV to Clay
- Run enrichments on entire table
- Write back to HubSpot (match on Contact ID)
Method 2: HubSpot List-Based Trigger
- Create HubSpot list: “Contacts Missing Enrichment Data”
- Filters:
- LinkedIn Profile URL is unknown
- OR Company Employee Count is unknown
- Filters:
- HubSpot Workflow: “Enroll in Clay Enrichment”
- Trigger: Contact is added to list
- Action: Send webhook to Clay
- Clay processes and writes back
- 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?
| Feature | Clay | Clearbit |
|---|---|---|
| Core Function | Enrichment orchestration (uses multiple sources) | Single enrichment provider |
| Data Sources | 50+ providers (including Clearbit) | Clearbit database only |
| Cost Optimization | Waterfall logic (uses cheapest source first) | Fixed per-lookup cost |
| Match Rate | 75-85% (combined sources) | 60-70% |
| Cost per Contact | $0.01-0.05 | $0.50-1.50 |
| Best For | High volume, cost-sensitive | Enterprise, brand data |
| HubSpot Integration | Native 2-way sync | Native 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.
Related Resources
Essential Reading
- Clay Waterfall Optimization Guide - Maximize match rates and minimize costs
- HubSpot API Documentation - Official API reference
- Data Enrichment ROI Calculator - Calculate your potential savings
- Clay vs Clearbit vs ZoomInfo Comparison (2025) - Detailed feature comparison
Integration Guides
- Clay to Salesforce Integration - Similar setup for SFDC users
- Clay Lead Scoring Guide - Build ICP scores from enriched data
- HubSpot Workflow Automation Best Practices - Advanced workflow patterns
- Clay + Slack Integration Guide - Real-time enrichment notifications
Community & Support
- Clay Community Slack - 12,000+ members, very active
- Clay Office Hours - Weekly Q&A with Clay team (Tuesdays 2pm ET)
- HubSpot Developer Forums - API troubleshooting
- Clay Academy - Free certification courses
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.
Need Implementation Help?
Our team can build this integration for you in 48 hours. From strategy to deployment.
Get Started