Auto-enrich new HubSpot contacts with Apollo using an agent skill

low complexityCost: Usage-based

Prerequisites

Prerequisites
  • Claude Code, Cursor, or another AI coding agent that supports skills
  • HubSpot private app token stored as HUBSPOT_TOKEN environment variable (scopes: crm.objects.contacts.read, crm.objects.contacts.write)
  • Apollo API key stored as APOLLO_API_KEY environment variable

Overview

Instead of maintaining a persistent automation, create an agent skill — a reusable instruction set that enriches HubSpot contacts via Apollo on demand. Run it whenever you want with /enrich-contacts, or schedule it with cron for hands-off operation.

Step 1: Create the skill directory

mkdir -p .claude/skills/enrich-contacts/scripts

Step 2: Write the SKILL.md file

Create .claude/skills/enrich-contacts/SKILL.md:

---
name: enrich-contacts
description: Enriches new HubSpot contacts with Apollo data. Finds contacts created recently, calls Apollo for job title, company, phone, and LinkedIn, then writes enriched fields back to HubSpot.
disable-model-invocation: true
allowed-tools: Bash(python *)
---
 
Enrich recently created HubSpot contacts with Apollo data:
 
1. Run: `python $SKILL_DIR/scripts/enrich.py`
2. Review the output for enrichment results and any errors
3. Confirm the number of contacts enriched

Step 3: Write the enrichment script

Create .claude/skills/enrich-contacts/scripts/enrich.py:

#!/usr/bin/env python3
"""
HubSpot Contact Enrichment via Apollo
Finds new contacts, enriches via Apollo People Match, writes back to HubSpot.
"""
import os
import sys
import time
from datetime import datetime, timedelta, timezone
 
try:
    import requests
except ImportError:
    os.system("pip install requests -q")
    import requests
 
HUBSPOT_TOKEN = os.environ.get("HUBSPOT_TOKEN")
APOLLO_API_KEY = os.environ.get("APOLLO_API_KEY")
 
if not all([HUBSPOT_TOKEN, APOLLO_API_KEY]):
    print("ERROR: Set HUBSPOT_TOKEN and APOLLO_API_KEY environment variables")
    sys.exit(1)
 
HS_HEADERS = {"Authorization": f"Bearer {HUBSPOT_TOKEN}", "Content-Type": "application/json"}
APOLLO_HEADERS = {"x-api-key": APOLLO_API_KEY, "Content-Type": "application/json"}
 
# --- Fetch new contacts ---
since = int((datetime.now(timezone.utc) - timedelta(hours=24)).timestamp() * 1000)
print(f"Searching for contacts created in the last 24 hours...")
 
contacts = []
after = 0
while True:
    resp = requests.post(
        "https://api.hubapi.com/crm/v3/objects/contacts/search",
        headers=HS_HEADERS,
        json={
            "filterGroups": [{"filters": [{
                "propertyName": "createdate",
                "operator": "GTE",
                "value": str(since)
            }]}],
            "properties": ["email", "firstname", "lastname", "jobtitle", "company"],
            "limit": 100,
            "after": after
        }
    )
    resp.raise_for_status()
    data = resp.json()
    contacts.extend(data["results"])
    if data.get("paging", {}).get("next"):
        after = data["paging"]["next"]["after"]
    else:
        break
 
print(f"Found {len(contacts)} new contacts")
 
# --- Enrich and update ---
enriched = 0
skipped = 0
 
for contact in contacts:
    email = contact["properties"].get("email")
    existing_title = contact["properties"].get("jobtitle")
 
    if not email:
        skipped += 1
        continue
 
    if existing_title:
        skipped += 1
        continue
 
    # Skip personal email domains
    domain = email.split("@")[-1].lower()
    if domain in ("gmail.com", "yahoo.com", "hotmail.com", "outlook.com", "aol.com"):
        skipped += 1
        continue
 
    # Call Apollo
    apollo_resp = requests.post(
        "https://api.apollo.io/api/v1/people/match",
        headers=APOLLO_HEADERS,
        json={"email": email}
    )
    apollo_resp.raise_for_status()
    person = apollo_resp.json().get("person")
 
    if not person:
        skipped += 1
        time.sleep(0.5)
        continue
 
    # Build update payload (only non-null fields)
    properties = {}
    if person.get("title"):
        properties["jobtitle"] = person["title"]
    if person.get("organization", {}).get("name"):
        properties["company"] = person["organization"]["name"]
    if person.get("phone_numbers") and person["phone_numbers"][0].get("sanitized_number"):
        properties["phone"] = person["phone_numbers"][0]["sanitized_number"]
    if person.get("linkedin_url"):
        properties["linkedin_url"] = person["linkedin_url"]
    if person.get("organization", {}).get("industry"):
        properties["industry"] = person["organization"]["industry"]
 
    if properties:
        update_resp = requests.patch(
            f"https://api.hubapi.com/crm/v3/objects/contacts/{contact['id']}",
            headers=HS_HEADERS,
            json={"properties": properties}
        )
        update_resp.raise_for_status()
        enriched += 1
        print(f"  Enriched: {email} -> {list(properties.keys())}")
 
    time.sleep(0.5)  # respect Apollo rate limits
 
print(f"\nDone. Enriched: {enriched}, Skipped: {skipped}")

Step 4: Run the skill

# Via Claude Code
/enrich-contacts
 
# Or directly
python .claude/skills/enrich-contacts/scripts/enrich.py

Step 5: Schedule it (optional)

Option A: Cron

# crontab -e — run every hour
0 * * * * cd /path/to/project && python .claude/skills/enrich-contacts/scripts/enrich.py >> /var/log/enrich.log 2>&1

Option B: GitHub Actions

name: Enrich HubSpot Contacts
on:
  schedule:
    - cron: '0 * * * *'
  workflow_dispatch: {}
jobs:
  enrich:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install requests
      - run: python .claude/skills/enrich-contacts/scripts/enrich.py
        env:
          HUBSPOT_TOKEN: ${{ secrets.HUBSPOT_TOKEN }}
          APOLLO_API_KEY: ${{ secrets.APOLLO_API_KEY }}

Cost

  • Apollo: 1 credit per enrichment. The script skips personal emails and already-enriched contacts to conserve credits.
  • HubSpot: Free within API rate limits.
  • Compute: Free on GitHub Actions (2,000 min/month free tier).
Credit-saving filters

The script filters out personal email domains and contacts with existing job titles before calling Apollo. Without these filters, you'd burn credits on contacts that either can't be enriched (personal emails) or don't need it (already have data). This typically saves 30-50% of credits.

When to use this approach

  • You want to enrich contacts right now without setting up automation infrastructure
  • You're experimenting with Apollo enrichment before committing to a platform
  • You want to run enrichment ad-hoc — "enrich the 50 contacts from today's event"
  • You want enrichment logic version-controlled in your repo

When to move to a dedicated tool

  • You need real-time enrichment the moment a contact is created (webhooks, not polling)
  • Non-technical team members need to modify the enrichment logic
  • You need visual monitoring and execution history

Need help implementing this?

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