Auto-enrich new HubSpot contacts with Apollo using n8n

medium complexityCost: $0-24/moRecommended

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 from Settings → Integrations → API (available on all paid plans)
  • n8n credentials configured for HubSpot (OAuth or API key)

Why n8n?

n8n gives you a visual workflow builder with full control over API calls and field mapping. Unlike Zapier, you can use HTTP Request nodes to call Apollo's API directly without paying for a premium Webhooks feature — it's included on every plan. Self-hosted n8n is free with unlimited executions, making it the most cost-effective option for teams enriching hundreds of contacts per month.

The trade-off is setup time. You'll configure HTTP headers, write JSON bodies, and handle null checks manually. If you want a plug-and-play experience with no API configuration, Zapier's native HubSpot integration is simpler (though more expensive per contact).

Step 1: Set up the HubSpot trigger

Create a new workflow in n8n. Add a HubSpot Trigger node:

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

This fires every time a new contact is created in HubSpot, regardless of source (form, import, API, manual entry).

Trigger polling interval

n8n's HubSpot Trigger uses polling, not webhooks. On n8n cloud, the default interval is 1 minute. Self-hosted defaults to 5 minutes. Adjust in the node settings under Poll Times.

Step 2: Call the Apollo People Enrichment API

Add an HTTP Request node to enrich the contact via Apollo:

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

The Apollo response includes:

  • person.title — job title
  • person.seniority — seniority level (e.g., "director", "vp")
  • person.organization.name — company name
  • person.phone_numbers[0].sanitized_number — phone
  • person.linkedin_url — LinkedIn profile URL
  • person.departments[] — department tags
  • person.organization.estimated_num_employees — company size range
  • person.organization.industry — industry
Apollo returns nested data

The enrichment response nests everything under person. In n8n expressions, access fields as $json.person.title, not $json.title. If the email doesn't match anyone, person will be null — add an IF node before the update step to handle this.

Step 3: Filter out empty results

Add an IF node to skip contacts Apollo couldn't match:

  • Condition: {{ $json.person }} is not empty

This prevents the workflow from trying to update HubSpot with null values and wasting API calls.

Step 4: Update the HubSpot contact

Add an HTTP Request node on the "true" branch to write enriched data back to HubSpot:

  • Method: PATCH
  • URL: https://api.hubapi.com/crm/v3/objects/contacts/{{ $('HubSpot Trigger').item.json.id }}
  • Authentication: HubSpot credential
  • Body (JSON):
{
  "properties": {
    "jobtitle": "{{ $json.person.title }}",
    "company": "{{ $json.person.organization.name }}",
    "phone": "{{ $json.person.phone_numbers[0]?.sanitized_number }}",
    "linkedin_url": "{{ $json.person.linkedin_url }}",
    "industry": "{{ $json.person.organization.industry }}",
    "numemployees": "{{ $json.person.organization.estimated_num_employees }}"
  }
}
Custom properties

linkedin_url and numemployees may not exist by default in HubSpot. Create custom contact properties in Settings → Properties before running the workflow, or the PATCH request will silently ignore unknown fields.

Null field handling

If Apollo returns a title but no phone number, you'll write null to the phone field. Use n8n expressions with fallback: {{ $json.person.phone_numbers[0]?.sanitized_number || '' }}. Better yet, use a Code node to build the properties object dynamically, only including fields that have values.

Step 5: Add error handling

  1. On the HTTP Request nodes, enable Settings → Retry On Fail with 2 retries and a 3-second wait
  2. Create a separate error workflow that sends you a Slack DM or email when enrichment fails
  3. Link it via Settings → Error Workflow on the main workflow

Step 6: Test and activate

  1. Click Execute Workflow to test with recent contacts
  2. Check the Apollo HTTP node output — verify you see person.title, person.organization, etc.
  3. Check the HubSpot PATCH output — verify a 200 response
  4. Open the contact in HubSpot to confirm fields were updated
  5. Toggle the workflow to Active

Troubleshooting

Cost

  • n8n cloud: Starts at $24/mo (2,500 executions). Each enrichment run uses ~4 executions (trigger + HTTP + IF + update).
  • Apollo: 1 credit per person enrichment. On the Basic plan ($49/mo), you get 900 credits/month. The Professional plan ($79/mo) includes 2,400 credits/month.
  • HubSpot: API calls are free within rate limits (150 requests per 10 seconds).
Apollo credit burn

Every new contact triggers an enrichment, including test contacts and duplicates. Add a filter early in the workflow to skip contacts with personal email domains (gmail.com, yahoo.com, etc.) if you only want to enrich business contacts. This can save 30-50% of your Apollo credits.

Common questions

How often does the n8n HubSpot trigger poll for new contacts?

On n8n cloud, the default polling interval is 1 minute. Self-hosted defaults to 5 minutes but can be configured down to 1 minute. This means enrichment happens within 1-5 minutes of contact creation — fast enough for most workflows, though not instant like a webhook.

How many contacts can I enrich per month on the free self-hosted plan?

There's no execution limit on self-hosted n8n. The bottleneck is Apollo credits: 900/month on the Basic plan ($49/mo), 2,400/month on Professional ($79/mo). At 1 credit per contact, the Basic plan handles ~225 contacts/week.

Can I use n8n's built-in HubSpot node instead of HTTP Request nodes?

Yes, for the trigger and basic updates. The built-in HubSpot node handles OAuth automatically. However, for custom properties (like linkedin_url) or for the Apollo API call, you'll need HTTP Request nodes since there's no native Apollo node in n8n.

Next steps

  • Add ICP filtering — insert a Code node after the trigger to check company size or domain before calling Apollo
  • Track enrichment status — set a custom HubSpot property like enrichment_source = apollo and enrichment_date to track which contacts were enriched
  • Enrich with company data — use Apollo's Organization Enrichment endpoint for deeper firmographic data on the associated company

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.