Waterfall enrich HubSpot contacts across Apollo, Clearbit, and PDL using n8n

high complexityCost: $0-24/mo

Prerequisites

Prerequisites
  • n8n instance — n8n cloud or self-hosted
  • HubSpot private app token with crm.objects.contacts.read and crm.objects.contacts.write scopes
  • Apollo API key (Settings → Integrations → API)
  • Clearbit API key (API → API Keys in dashboard)
  • People Data Labs API key (from PDL dashboard)
  • n8n credential configured for HubSpot

Why n8n?

n8n's IF nodes and Code nodes make the waterfall cascade visible and debuggable. You can see exactly which provider was called for each contact, what data it returned, and where the merge happened. Each branch is a distinct path on the canvas, making it easy to trace why a specific contact ended up with "apollo+clearbit" as its source.

The trade-off is node count. A full three-provider waterfall requires 10+ nodes — HTTP requests, Code nodes for merging, IF nodes for branching, and a Merge node to converge the paths. If you want the waterfall as a single readable function, the Code approach is simpler.

Step 1: Trigger on new contacts

Add a HubSpot Trigger node:

  • Trigger event: Contact Created
  • Authentication: Your HubSpot credential

Alternatively, use a Schedule Trigger to batch-enrich on a schedule (see Recipe 5 for the batch pattern).

Step 2: Call Apollo People Enrichment (first provider)

Add an HTTP Request node:

  • Method: POST
  • URL: https://api.apollo.io/api/v1/people/match
  • Headers:
    • x-api-key: Your Apollo API key
    • Content-Type: application/json
  • Body:
{
  "email": "{{ $json.properties.email }}"
}

Step 3: Track which fields are still missing

Add a Code node (Run Once for All Items) to evaluate what Apollo returned and flag gaps:

const person = $input.first().json.person;
const contactId = $('HubSpot Trigger').first().json.id;
const email = $('HubSpot Trigger').first().json.properties.email;
 
const fields = {
  jobtitle: person?.title || null,
  company: person?.organization?.name || null,
  phone: person?.phone_numbers?.[0]?.sanitized_number || null,
  linkedin_url: person?.linkedin_url || null,
  industry: person?.organization?.industry || null,
  seniority: person?.seniority || null,
};
 
const missingFields = Object.entries(fields)
  .filter(([_, v]) => !v)
  .map(([k]) => k);
 
return [{
  json: {
    contactId,
    email,
    fields,
    missingFields,
    source: "apollo",
    needsFallback: missingFields.length > 0,
  }
}];

Step 4: Branch to Clearbit if fields are missing

Add an IF node:

  • Condition: {{ $json.needsFallback }} equals true

On the true branch, add an HTTP Request node for Clearbit:

  • Method: GET
  • URL: https://person.clearbit.com/v2/people/find?email={{ $json.email }}
  • Headers:
    • Authorization: Bearer YOUR_CLEARBIT_API_KEY
Clearbit auth format

Clearbit uses Bearer token auth, not Basic auth. Pass your API key as Bearer sk_... in the Authorization header.

Add a second Code node to merge Clearbit data into any remaining gaps:

const existing = $('Check Missing Fields').first().json;
const clearbit = $input.first().json;
const fields = { ...existing.fields };
 
// Only fill fields that are still null
if (!fields.jobtitle && clearbit.title) fields.jobtitle = clearbit.title;
if (!fields.company && clearbit.employment?.name) fields.company = clearbit.employment.name;
if (!fields.seniority && clearbit.employment?.seniority) fields.seniority = clearbit.employment.seniority;
if (!fields.linkedin_url && clearbit.linkedin?.handle) {
  fields.linkedin_url = `https://linkedin.com/in/${clearbit.linkedin.handle}`;
}
 
const stillMissing = Object.entries(fields)
  .filter(([_, v]) => !v)
  .map(([k]) => k);
 
return [{
  json: {
    ...existing,
    fields,
    missingFields: stillMissing,
    source: stillMissing.length < existing.missingFields.length ? "apollo+clearbit" : existing.source,
    needsFallback: stillMissing.length > 0,
  }
}];

Step 5: Fall back to People Data Labs for remaining gaps

Add another IF node on the Clearbit output:

  • Condition: {{ $json.needsFallback }} equals true

On the true branch, add an HTTP Request node for PDL:

  • Method: POST
  • URL: https://api.peopledatalabs.com/v5/person/enrich
  • Headers:
    • x-api-key: Your PDL API key
    • Content-Type: application/json
  • Body:
{
  "email": "{{ $json.email }}"
}

Add a Code node to merge PDL data:

const existing = $input.first().json;
const pdl = $('PDL Enrichment').first().json.data || $('PDL Enrichment').first().json;
const fields = { ...existing.fields };
 
if (!fields.jobtitle && pdl.job_title) fields.jobtitle = pdl.job_title;
if (!fields.company && pdl.job_company_name) fields.company = pdl.job_company_name;
if (!fields.phone && pdl.phone_numbers?.[0]) fields.phone = pdl.phone_numbers[0];
if (!fields.linkedin_url && pdl.linkedin_url) fields.linkedin_url = pdl.linkedin_url;
if (!fields.industry && pdl.industry) fields.industry = pdl.industry;
 
const sources = [existing.source, "pdl"].filter(Boolean);
 
return [{
  json: {
    ...existing,
    fields,
    source: sources.join("+"),
  }
}];
PDL response structure

People Data Labs nests the person data under a data key when called via the REST API. Check for both response.data and the top-level response, as the structure varies between direct API calls and SDK usage.

Step 6: Write enriched data to HubSpot

All three branches (Apollo-only, Apollo+Clearbit, Apollo+Clearbit+PDL) converge here. Add a Merge node set to Merge by Position to combine the branches, then an HTTP Request node:

  • Method: PATCH
  • URL: https://api.hubapi.com/crm/v3/objects/contacts/{{ $json.contactId }}
  • Authentication: HubSpot credential
  • Body:
{
  "properties": {
    "jobtitle": "{{ $json.fields.jobtitle }}",
    "company": "{{ $json.fields.company }}",
    "phone": "{{ $json.fields.phone }}",
    "linkedin_url": "{{ $json.fields.linkedin_url }}",
    "industry": "{{ $json.fields.industry }}",
    "enrichment_source": "{{ $json.source }}"
  }
}
Track the source

Write the enrichment_source (e.g., "apollo", "apollo+clearbit", "apollo+clearbit+pdl") to a custom HubSpot property. This tells you which provider filled which contacts — invaluable for evaluating provider ROI.

Step 7: Test and activate

  1. Click Execute Workflow to run against a recent contact
  2. Check each Code node's output to verify field merging works correctly
  3. Confirm the HubSpot contact was updated with the correct source attribution
  4. Toggle the workflow to Active

Troubleshooting

Cost

  • n8n cloud: Starts at $24/mo. Each waterfall run uses 5-10 executions depending on how many providers are called.
  • Apollo: 1 credit per enrichment ($49/mo Basic = 900 credits)
  • Clearbit: Starts at $99/mo for the Enrichment API. Pricing is volume-based.
  • People Data Labs: $0.03-0.10 per enrichment depending on plan. Pay-as-you-go available.
  • Best case: Apollo matches (1 credit). Worst case: All three providers called (1 Apollo + 1 Clearbit + 1 PDL credit).
Minimize credit spend

The waterfall pattern is designed to minimize cost — you only call Clearbit if Apollo missed fields, and only call PDL if both missed. On average, Apollo covers 60-70% of B2B contacts fully, so you'll only call Clearbit for ~30% and PDL for ~10-15%.

Common questions

How many n8n executions does a single contact use through the full waterfall?

5-10 executions depending on how many providers are called. Apollo-only (all fields filled) uses about 5 executions. A full cascade through all three providers uses about 10. On n8n cloud, this is well within plan limits for most batch sizes.

Can I remove Clearbit or PDL from the waterfall?

Yes. Simply delete the IF node and HTTP Request node for the provider you want to remove. The remaining branches still work because each merge only fills gaps — removing a provider just means fewer chances to fill missing fields.

How do I track which provider gives the best ROI?

The enrichment_source field on each contact tells you which providers contributed. After a month of enrichment, query HubSpot for contacts by enrichment_source to see: how many were "apollo" only, how many needed "apollo+clearbit", and how many required all three. This tells you whether Clearbit and PDL are worth their subscription cost.

Next steps

  • Add enrichment scoring — track fill rate per provider to evaluate if you're getting value from each
  • Add a dead-letter queue — store contacts that all three providers missed for manual research
  • Reorder providers — if Clearbit consistently fills more fields than Apollo for your ICP, swap their order

Looking to scale your AI operations?

We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.