Find and verify emails for HubSpot prospects using Apollo and Hunter with code
Prerequisites
- Node.js 18+ or Python 3.9+
- HubSpot private app token with
crm.objects.contacts.readandcrm.objects.contacts.writescopes - Apollo API key with email finder credits
- Hunter.io API key with verification credits
- A scheduling environment: cron, GitHub Actions, or a cloud function
Why code?
A script gives you the most cost-effective path for batch email finding. Zero platform cost, full control over the find→verify→update pipeline, and the ability to add custom logic like domain resolution, catch-all detection, and CSV export. GitHub Actions provides free scheduling.
The trade-off is maintenance. You own the error handling, rate limiting, and monitoring. There's no visual execution history. If non-technical team members need to trigger or modify the process, use n8n or Make instead.
Step 1: Set up the project
# Test Apollo people match
curl -X POST "https://api.apollo.io/api/v1/people/match" \
-H "x-api-key: $APOLLO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"first_name": "Tim", "last_name": "Cook", "organization_name": "Apple"}'
# Test Hunter email verification
curl "https://api.hunter.io/v2/email-verifier?email=test@example.com&api_key=$HUNTER_API_KEY"Step 2: Find contacts missing emails in HubSpot
import requests
import os
import time
HUBSPOT_ACCESS_TOKEN = os.environ["HUBSPOT_ACCESS_TOKEN"]
APOLLO_API_KEY = os.environ["APOLLO_API_KEY"]
HUNTER_API_KEY = os.environ["HUNTER_API_KEY"]
HS_HEADERS = {"Authorization": f"Bearer {HUBSPOT_ACCESS_TOKEN}", "Content-Type": "application/json"}
def get_contacts_without_email(limit=50):
all_contacts = []
after = 0
while len(all_contacts) < limit:
resp = requests.post(
"https://api.hubapi.com/crm/v3/objects/contacts/search",
headers=HS_HEADERS,
json={
"filterGroups": [{"filters": [
{"propertyName": "email", "operator": "NOT_HAS_PROPERTY"},
{"propertyName": "firstname", "operator": "HAS_PROPERTY"},
{"propertyName": "company", "operator": "HAS_PROPERTY"},
]}],
"properties": ["firstname", "lastname", "company", "domain"],
"limit": min(100, limit - len(all_contacts)),
"after": after
}
)
resp.raise_for_status()
data = resp.json()
all_contacts.extend(data["results"])
if data.get("paging", {}).get("next"):
after = data["paging"]["next"]["after"]
else:
break
return all_contactsStep 3: Find emails via Apollo
def find_email_apollo(first_name, last_name, company):
"""Find a person's email via Apollo People Match."""
resp = requests.post(
"https://api.apollo.io/api/v1/people/match",
headers={"x-api-key": APOLLO_API_KEY, "Content-Type": "application/json"},
json={
"first_name": first_name,
"last_name": last_name,
"organization_name": company
}
)
resp.raise_for_status()
person = resp.json().get("person")
if not person or not person.get("email"):
return None, None
return person["email"], person.get("email_status", "unknown")Step 4: Verify with Hunter
Skip verification if Apollo already marked the email as verified:
def verify_email_hunter(email):
"""Verify an email via Hunter. Returns 'deliverable', 'risky', 'undeliverable', or 'unknown'."""
resp = requests.get(
"https://api.hunter.io/v2/email-verifier",
params={"email": email, "api_key": HUNTER_API_KEY}
)
resp.raise_for_status()
return resp.json()["data"]["result"]
def find_and_verify(first_name, last_name, company):
"""Find email via Apollo, verify via Hunter if needed."""
email, apollo_status = find_email_apollo(first_name, last_name, company)
if not email:
return {"email": None, "status": "not_found", "source": "apollo"}
# Skip Hunter if Apollo already verified
if apollo_status == "verified":
return {"email": email, "status": "verified", "source": "apollo"}
# Verify with Hunter
hunter_result = verify_email_hunter(email)
return {
"email": email,
"status": hunter_result,
"source": "apollo+hunter"
}Apollo returns verified (confirmed deliverable), guessed (pattern-matched), unavailable (no email found), or null. Only verified emails can safely skip Hunter verification.
Step 5: Process contacts and update HubSpot
def update_contact_email(contact_id, email, status, source):
"""Write email and verification status to HubSpot."""
properties = {"email_source": source, "email_verification_status": status}
if status == "deliverable" or status == "verified":
properties["email"] = email
resp = requests.patch(
f"https://api.hubapi.com/crm/v3/objects/contacts/{contact_id}",
headers=HS_HEADERS,
json={"properties": properties}
)
resp.raise_for_status()
def main():
contacts = get_contacts_without_email(limit=50)
print(f"Found {len(contacts)} contacts without email\n")
stats = {"found": 0, "verified": 0, "risky": 0, "not_found": 0}
for contact in contacts:
props = contact["properties"]
name = f"{props.get('firstname', '')} {props.get('lastname', '')}".strip()
company = props.get("company", "")
result = find_and_verify(
props.get("firstname", ""),
props.get("lastname", ""),
company
)
if result["status"] in ("deliverable", "verified"):
update_contact_email(contact["id"], result["email"], result["status"], result["source"])
stats["verified"] += 1
print(f" {name} @ {company} -> {result['email']} ({result['source']})")
elif result["status"] == "risky":
update_contact_email(contact["id"], result["email"], "risky", result["source"])
stats["risky"] += 1
print(f" {name} @ {company} -> {result['email']} (RISKY)")
else:
stats["not_found"] += 1
print(f" {name} @ {company} -> not found")
time.sleep(0.5) # rate limit
print(f"\nDone. Verified: {stats['verified']}, Risky: {stats['risky']}, Not found: {stats['not_found']}")
if __name__ == "__main__":
main()Step 6: Schedule the script
# .github/workflows/find-emails.yml
name: Find and Verify Emails
on:
schedule:
- cron: '0 11 * * *' # 6 AM ET = 11 AM UTC
workflow_dispatch: {}
jobs:
find-emails:
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 find_emails.py
env:
HUBSPOT_ACCESS_TOKEN: ${{ secrets.HUBSPOT_ACCESS_TOKEN }}
APOLLO_API_KEY: ${{ secrets.APOLLO_API_KEY }}
HUNTER_API_KEY: ${{ secrets.HUNTER_API_KEY }}Rate limits
| API | Limit | Delay |
|---|---|---|
| Apollo People Match | 5 req/sec (Basic) | 500ms between calls |
| Hunter Email Verifier | 15 req/sec | No delay needed at 50/batch |
| HubSpot Search | 5 req/sec | 200ms between pages |
| HubSpot PATCH | 150 req/10 sec | No delay needed |
Troubleshooting
Cost
- Apollo: 1 credit per people match. Basic plan ($49/mo) = 900 credits.
- Hunter: 1 credit per verification. Starter plan ($49/mo) = 1,000 verifications. Free plan = 25/month.
- Credit savings: Apollo-verified emails skip Hunter entirely. Typically 30-40% of Apollo results are pre-verified, saving that many Hunter credits.
- Per 50 contacts: 50 Apollo credits + ~30-35 Hunter credits (after skipping verified ones).
Apollo charges 1 credit even when no person is found. If your contact list has low-quality data (misspelled names, wrong companies), you'll burn credits on misses. Clean your data before running the script.
Common questions
How much does this cost per 50 contacts?
50 Apollo credits (~$2.72 on Basic plan) + 30-35 Hunter credits ($1.47 on Starter plan) = ~$4.19 total. GitHub Actions and HubSpot API calls are free.
Should I run this daily or weekly?
Depends on your lead volume. If you import 10-20 contacts per day, daily runs make sense. If contacts come in weekly batches (trade shows, list imports), run it once after each import.
What's the best way to handle catch-all domains?
Hunter returns "unknown" for catch-all domains where any address is accepted. The script treats these as unverified. For high-value prospects at catch-all domains, store the email with a "risky" flag and let reps decide whether to use it.
Next steps
- Add domain resolution — for contacts without a company domain, use Hunter's Domain Search (
GET /v2/domain-search?company=Acme+Inc) to find it first - Export results — write a CSV summary for review: name, company, email found, verification status, source
- Add bounce tracking — after emails are used in sequences, track bounces and feed them back to update verification status
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.