Find and verify emails for HubSpot prospects using Apollo and Hunter with a Claude Code skill
Install this skill
Download the skill archive and extract it into your .claude/skills/ directory.
email-finder.skill.zipPrerequisites
This skill works with any agent that supports the Claude Code skills standard, including Claude Code, Claude Cowork, OpenAI Codex, and Google Antigravity.
- One of the agents listed above
- HubSpot private app with
crm.objects.contacts.readandcrm.objects.contacts.writescopes - Apollo API key with email finder credits (Settings → Integrations → API)
- Hunter.io API key with verification credits (Dashboard → API)
Why a Claude Code skill?
The other approaches in this guide are deterministic: they run the same logic every time, the same way. An Claude Code skill is different. You tell Claude what you want in plain language, and the skill gives it enough context to do it reliably.
That means you can say:
- "Find emails for the trade show contacts imported yesterday"
- "How many contacts are still missing emails?"
- "Find and verify emails for contacts at companies with 100+ employees"
- "Do a dry run — show me what would be found but don't update HubSpot"
The skill contains workflow guidelines, API reference materials, and a field mapping template that the agent reads on demand. When you invoke the skill, Claude reads these files, writes a script on the fly, runs it, and reports results. If you ask for something different next time — a different contact filter, skip verification, only process a specific company — the agent adapts without you touching any code.
How it works
The skill directory has three parts:
SKILL.md— workflow guidelines telling the agent what steps to follow, which env vars to use, and what pitfalls to avoidreferences/— Apollo People Match and Hunter Email Verifier API patterns so the agent calls the right APIs with the right parameterstemplates/— a field mapping template so results are written to the correct HubSpot properties consistently
When invoked, the agent reads SKILL.md, consults the reference and template files as needed, writes a Python script, executes it, and reports what it found. The reference files act as guardrails — the agent knows exactly which endpoints to hit and what the responses look like.
What is a Claude Code skill?
An Claude Code skill is a reusable command you add to your project that Claude Code can run on demand. Skills live in a .claude/skills/ directory and are defined by a SKILL.md file that tells the agent what the skill does, when to run it, and what tools it's allowed to use.
In this skill, the agent doesn't run a pre-written script. Instead, SKILL.md provides workflow guidelines and points to reference files — API documentation, field mappings — that the agent reads to generate and execute code itself. This is the key difference from a traditional script: the agent can adapt its approach based on what you ask for while still using the right APIs and field formats.
Once installed, you can invoke a skill as a slash command (e.g., /find-emails), or the agent will use it automatically when you give it a task where the skill is relevant. Skills are portable — anyone who clones your repo gets the same commands.
Step 1: Create the skill directory
mkdir -p .claude/skills/find-emails/{templates,references}This creates the layout:
.claude/skills/find-emails/
├── SKILL.md # workflow guidelines + config
├── templates/
│ └── field-mapping.md # HubSpot field mapping for email results
└── references/
├── apollo-people-match.md # Apollo People Match API patterns
├── hunter-email-verifier.md # Hunter Email Verifier API patterns
└── hubspot-contacts-api.md # HubSpot CRM Search + Update patternsStep 2: Write the SKILL.md
Create .claude/skills/find-emails/SKILL.md:
---
name: find-emails
description: Find and verify work emails for HubSpot contacts missing email addresses using Apollo and Hunter
disable-model-invocation: true
allowed-tools: Bash, Read
---
## Goal
Find HubSpot contacts that are missing email addresses, look up their work email via Apollo's People Match API, verify deliverability with Hunter's Email Verifier API, and write verified emails back to HubSpot.
## Configuration
Read these environment variables:
- `HUBSPOT_ACCESS_TOKEN` — HubSpot private app token (required)
- `APOLLO_API_KEY` — Apollo API key (required)
- `HUNTER_API_KEY` — Hunter.io API key (required)
Default behavior: find all contacts missing an email that have both a first name and company. The user may request a different filter, a specific list, or a dry-run mode.
## Workflow
1. Validate that all required env vars are set. If any are missing, print which ones and exit.
2. Search HubSpot for contacts matching the criteria (missing email, has name and company). See `references/hubspot-contacts-api.md` for the Search API.
3. For each contact, call Apollo's People Match endpoint with first name, last name, and company. See `references/apollo-people-match.md`.
4. If Apollo returns an email with `email_status: "verified"`, skip Hunter verification.
5. For unverified Apollo emails, call Hunter's Email Verifier. See `references/hunter-email-verifier.md`.
6. Write results to HubSpot using the mapping in `templates/field-mapping.md`. Only write verified or deliverable emails. Flag risky emails.
7. Print a summary: how many found, verified, risky, and not found.
## Important notes
- Apollo charges 1 credit per People Match call even when no person is found. Clean data (accurate names, correct company names) improves match rates.
- Apollo's match rate is significantly better with a company domain than a company name. If the contact has a `domain` property, use it.
- Hunter returns `unknown` for catch-all domains. Treat these as unverified but potentially valid.
- Apollo rate limit: 5 requests/second on Basic. Add 0.5-second delays between calls.
- Hunter rate limit: 15 requests/second. No delay needed for small batches.
- Use the `requests` library for HTTP calls. Install with pip if needed.Understanding the SKILL.md
| Section | Purpose |
|---|---|
| Goal | Tells the agent what outcome to produce |
| Configuration | Which env vars to read and what defaults to use |
| Workflow | Numbered steps with pointers to reference files |
| Important notes | Non-obvious context that prevents common mistakes |
The allowed-tools: Bash, Read setting lets the agent both read reference files and execute code.
Step 3: Add reference files
references/apollo-people-match.md
Create .claude/skills/find-emails/references/apollo-people-match.md:
# Apollo People Match API Reference
## Find a person by name and company
**Request:**
```
POST https://api.apollo.io/api/v1/people/match
x-api-key: <APOLLO_API_KEY>
Content-Type: application/json
```
**Body (name + company):**
```json
{
"first_name": "Jane",
"last_name": "Doe",
"organization_name": "Acme Corp"
}
```
**Body (name + domain — higher accuracy):**
```json
{
"first_name": "Jane",
"last_name": "Doe",
"domain": "acme.com"
}
```
**Response shape (matched):**
```json
{
"person": {
"email": "jdoe@acme.com",
"email_status": "verified",
"title": "VP of Engineering",
"organization": {
"name": "Acme Corp"
}
}
}
```
**email_status values:**
- `verified` — confirmed deliverable by Apollo (safe to skip Hunter)
- `guessed` — pattern-matched, not confirmed (needs Hunter verification)
- `unavailable` — no email found
- `null` — status unknown
**Response shape (no match):**
```json
{
"person": null
}
```
## Notes
- Rate limit: 5 requests/second on Basic, 10/sec on Professional.
- 1 credit per call regardless of match result.
- Using `domain` instead of `organization_name` significantly improves match rates.references/hunter-email-verifier.md
Create .claude/skills/find-emails/references/hunter-email-verifier.md:
# Hunter Email Verifier API Reference
## Verify a single email
**Request:**
```
GET https://api.hunter.io/v2/email-verifier?email=<EMAIL>&api_key=<HUNTER_API_KEY>
```
**Response shape:**
```json
{
"data": {
"result": "deliverable",
"score": 95,
"email": "jdoe@acme.com",
"regexp": true,
"gibberish": false,
"disposable": false,
"webmail": false,
"mx_records": true,
"smtp_server": true,
"smtp_check": true,
"accept_all": false,
"block": false
}
}
```
**result values:**
- `deliverable` — safe to send to
- `risky` — might bounce (catch-all domain, accept_all=true)
- `undeliverable` — will bounce
- `unknown` — couldn't determine (common with catch-all domains)
## Notes
- Rate limit: 15 requests/second.
- 1 credit per verification on paid plans.
- Free plan: 25 verifications/month. Starter: $49/mo for 1,000.
- `accept_all: true` means the domain accepts all addresses — verification can't confirm a specific mailbox exists.references/hubspot-contacts-api.md
Create .claude/skills/find-emails/references/hubspot-contacts-api.md:
# HubSpot Contacts API Reference
## Search for contacts missing email
**Request:**
```
POST https://api.hubapi.com/crm/v3/objects/contacts/search
Authorization: Bearer <HUBSPOT_ACCESS_TOKEN>
Content-Type: application/json
```
**Body:**
```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": 100
}
```
- Filters within the same `filterGroup` are ANDed together.
- `limit` max is 100. Use `paging.next.after` to paginate.
## Update a contact
```
PATCH https://api.hubapi.com/crm/v3/objects/contacts/<CONTACT_ID>
Authorization: Bearer <HUBSPOT_ACCESS_TOKEN>
Content-Type: application/json
```
**Body:**
```json
{
"properties": {
"email": "jdoe@acme.com",
"email_source": "apollo+hunter",
"email_verification_status": "verified"
}
}
```
- Only include properties you want to update.
- Custom properties (`email_source`, `email_verification_status`) must exist in HubSpot first.templates/field-mapping.md
Create .claude/skills/find-emails/templates/field-mapping.md:
# Email Finder → HubSpot Field Mapping
| Source | HubSpot property | Notes |
|--------|-----------------|-------|
| Found email | `email` | Only write if verification result is "deliverable" or "verified" |
| Source attribution | `email_source` | "apollo" (if Apollo-verified) or "apollo+hunter" (if Hunter-verified). Custom property. |
| Verification result | `email_verification_status` | "verified", "risky", "undeliverable", or "not_found". Custom property. |
## Rules
- **Only write verified or deliverable emails** to the `email` field. Risky emails may be stored with a flag but should be reviewed before use.
- Set `email_verification_status` for every processed contact, even when no email was found (use "not_found") to prevent re-processing.
- Custom properties must be created in HubSpot before the script runs.Step 4: Test the skill
Invoke the skill conversationally:
/find-emailsClaude will read the SKILL.md, check the reference files, write a script, install any missing dependencies, run it, and report the results. A typical run looks like:
Searching for contacts without email addresses...
Found 25 contacts with name + company but no email
Processing 25 contacts...
Jane Doe @ Acme Corp → jdoe@acme.com (apollo-verified)
Mike Chen @ WidgetCo → mchen@widgetco.io (hunter: deliverable)
Sarah Kim @ BigCo → skim@bigco.com (hunter: risky)
Tom Brown @ SmallShop → not found
...
Done. Verified: 18, Risky: 3, Not found: 4. Skipped Hunter for 8 Apollo-verified emails.Because the agent generates code on the fly, you can also make ad hoc requests:
- "Find emails for the contacts imported yesterday" — the agent adds a createdate filter
- "Only process contacts at companies with 100+ employees" — the agent adds a numemployees filter
- "Do a dry run" — the agent shows what would be found without updating HubSpot
- "How many contacts are still missing emails?" — the agent queries and reports without processing
Create a test contact in HubSpot with a first name, last name, and company (use a well-known company like "Google" or "Salesforce") but no email. Run the skill and verify the email is found and verified.
Step 5: Schedule it (optional)
Option A: Cron + Claude CLI
# Run daily at 6 AM
0 6 * * * cd /path/to/your/project && claude -p "Run /find-emails" --allowedTools 'Bash(*)' 'Read(*)'Option B: GitHub Actions + Claude
name: Find and Verify Emails
on:
schedule:
- cron: '0 11 * * *' # Daily at 6 AM ET
workflow_dispatch: {} # Manual trigger for testing
jobs:
find:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: anthropics/claude-code-action@v1
with:
prompt: "Run /find-emails"
allowed_tools: "Bash(*),Read(*)"
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
HUBSPOT_ACCESS_TOKEN: ${{ secrets.HUBSPOT_ACCESS_TOKEN }}
APOLLO_API_KEY: ${{ secrets.APOLLO_API_KEY }}
HUNTER_API_KEY: ${{ secrets.HUNTER_API_KEY }}0 11 * * * is 11 AM UTC = 6 AM ET. GitHub Actions cron may have up to 15 minutes of delay.
Option C: Cowork Scheduled Tasks
Claude Desktop's Cowork supports built-in scheduled tasks. Open a Cowork session, type /schedule, and configure the cadence — hourly, daily, weekly, or weekdays only. Each scheduled run has full access to your connected tools, plugins, and MCP servers.
Scheduled tasks only run while your computer is awake and Claude Desktop is open. If a run is missed, Cowork executes it automatically when the app reopens. For always-on scheduling, use GitHub Actions (Option B) instead. Available on all paid plans (Pro, Max, Team, Enterprise).
Troubleshooting
When to use this approach
- You want conversational flexibility — "find emails for the trade show list" or "only process enterprise contacts"
- You want on-demand email finding for specific batches, not continuous automation
- You're already using Claude Code and want skills that integrate with your workflow
- You want to run tasks in the background via Claude Cowork while focusing on other work
- You prefer guided references over rigid scripts — the agent adapts while staying reliable
When to switch approaches
- You need automatic email finding for every new contact as it's created → use n8n with the HubSpot trigger
- You want a no-code setup with visual branching → use Make with Router modules
- You need email finding running 24/7 with zero LLM cost → use the Code + Cron approach
Common questions
Does this use Claude API credits?
Yes. The agent reads skill files and generates code each time. Typical cost is $0.02-0.08 per invocation (higher than enrichment because the find→verify→update chain requires more reference files). Apollo and Hunter have their own per-call pricing.
How is this different from the Code + Cron approach?
The Code + Cron approach runs a fixed script that always processes the same filter. The Claude Code skill adapts — you can ask for specific contact lists, different filters, dry-run mode, or summary reports. The reference files ensure it calls the right APIs even when improvising.
Can I skip Hunter verification entirely?
Yes. Ask the agent to "only use Apollo, skip Hunter verification." The agent will adapt the workflow to write Apollo's email directly to HubSpot, using Apollo's email_status field for confidence. This saves Hunter credits but increases bounce risk for non-verified emails.
What if I need to find emails for thousands of contacts?
For large batches, the Code + Cron approach with explicit pagination and rate limiting is more predictable. The Claude Code skill works well for batches under 200 contacts. For larger runs, ask the agent to process in batches of 50 with progress reporting.
Cost
- Claude API — $0.02-0.08 per invocation (the agent reads multiple reference files and generates code)
- Apollo — 1 credit per lookup, even for no-match results. Basic plan ($49/mo) = 900 credits.
- Hunter — 1 credit per verification. Starter plan ($49/mo) = 1,000 verifications. Free plan = 25/month.
- Typical batch of 50 contacts — 50 Apollo credits + ~30-35 Hunter credits (after skipping Apollo-verified) = ~$4-5 total
- GitHub Actions (if scheduled) — free tier includes 2,000 minutes/month
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.