Apollo.io API Guide 2026: From Zero to 500+ Verified Emails
Apollo.io has a 265M+ contact database accessible via API. I used it to find 580 verified decision-maker emails in one afternoon. This is everything I learned — including the gotchas that aren't in their docs.
Authentication
All requests use the X-Api-Key header:
curl -X POST "https://api.apollo.io/api/v1/..." \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json"
Get your key from Settings → API Keys in your Apollo dashboard.
The Two Endpoints You Actually Need
Apollo has dozens of endpoints. You only need two for lead generation:
1. Search: Find People
/api/v1/mixed_people/api_search — NOT /api/v1/mixed_people/search. The latter returns 403 on Basic plans ($59/mo). This isn't documented anywhere obvious. I wasted 30 minutes on it.
curl -X POST "https://api.apollo.io/api/v1/mixed_people/api_search" \
-H "X-Api-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"q_organization_keyword_tags": ["aesthetic clinic", "med spa"],
"person_titles": ["Owner", "Founder", "CEO"],
"person_locations": ["United States"],
"per_page": 100,
"page": 1
}'
What Search Returns (And What It Doesn't)
Search results include:
id— Apollo's internal person IDfirst_name— First name in plain textlast_name_obfuscated— Like "Do***t" (hidden on Basic)title— Job titleorganization.name— Company namehas_email— Boolean flag
Search does NOT return emails. You need to enrich each person to get their email. This is by design — enrichments cost credits.
2. Enrich: Get the Email
curl -X POST "https://api.apollo.io/api/v1/people/match" \
-H "X-Api-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"id": "APOLLO_PERSON_ID"}'
This returns the full person object including email, email_status (verified/unverified), name, title, and organization.
Cost: 1 credit per enrichment. On Basic ($59/mo), you get 2,500 credits/month.
has_email: true. In my tests, ~70% of search results had this flag, and ~80% of those yielded a verified email. That's a 56% overall hit rate.
The Search → Enrich Workflow
# Step 1: Search for people
search_results = apollo_search(titles, keywords, location)
# Step 2: Filter for has_email = true
enrichable = [p for p in search_results if p["has_email"]]
# Step 3: Enrich each one
for person in enrichable:
data = apollo_enrich(person["id"])
if data.get("email"):
save_to_csv(data)
My Numbers
| Metric | Value |
|---|---|
| Total searches | 900 people across 9 queries |
| Had email flag | ~630 (70%) |
| Enriched | ~430 |
| Got verified email | 580 (some found in multiple searches) |
| Credits used | ~430 out of 2,500 |
| Cost per email | ~$0.10 |
Bulk Enrichment: The Trap
Apollo has a /api/v1/people/bulk_match endpoint that looks faster. Don't use it with IDs from search results.
I tried passing Apollo person IDs to bulk_match. Every result came back null. Zero credits consumed. The IDs from api_search don't work with bulk_match.
bulk_match works with first_name + last_name + organization_name. But Apollo search obfuscates last names on Basic plans. So you're stuck using single people/match calls.
people/match with the person ID. It costs the same (1 credit) but actually works. Each call takes ~1 second.
Search Parameters That Matter
Job Titles
Use specific titles, not just "manager." For clinic outreach, I used:
"person_titles": ["Owner", "Founder", "CEO", "Medical Director", "Managing Partner"]
For DACH markets, include German titles:
"person_titles": ["Owner", "Founder", "Inhaber", "Geschäftsführer"]
Organization Keywords
"q_organization_keyword_tags": ["aesthetic clinic", "med spa", "plastic surgery", "dermatology"]
These search the company's industry tags and description. More specific = better quality results.
Location
"person_locations": ["United States"]
// or multiple:
"person_locations": ["Germany", "Switzerland", "Austria"]
Company Size
"organization_num_employees_ranges": ["1,10", "11,20", "21,50"]
Useful for targeting small businesses where the owner is the decision-maker.
Pagination
Results max out at 100 per page. Use the page parameter:
{"per_page": 100, "page": 1} // First 100
{"per_page": 100, "page": 2} // Next 100
The response includes total_entries so you know how many pages exist.
Rate Limits
Apollo doesn't publish explicit rate limits, but in practice:
- Search: ~1 request/second is safe
- Enrich: ~1 request/second (each takes ~1s anyway)
- If you hit 429, wait 60 seconds
Complete Python Script
import json, subprocess, csv
API_KEY = "YOUR_APOLLO_API_KEY"
def search(titles, keywords, location, page=1):
r = subprocess.run(['curl', '-s', '-X', 'POST',
'https://api.apollo.io/api/v1/mixed_people/api_search',
'-H', f'X-Api-Key: {API_KEY}',
'-H', 'Content-Type: application/json',
'-d', json.dumps({
"q_organization_keyword_tags": keywords,
"person_titles": titles,
"person_locations": [location],
"per_page": 100, "page": page
})], capture_output=True, text=True)
return json.loads(r.stdout)
def enrich(person_id):
r = subprocess.run(['curl', '-s', '-X', 'POST',
'https://api.apollo.io/api/v1/people/match',
'-H', f'X-Api-Key: {API_KEY}',
'-H', 'Content-Type: application/json',
'-d', json.dumps({"id": person_id})
], capture_output=True, text=True)
return json.loads(r.stdout).get("person", {})
# Search
data = search(
["Owner", "Founder", "CEO"],
["aesthetic clinic", "med spa"],
"United States"
)
# Enrich and save
with open("leads.csv", "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=["name","email","title","company"])
w.writeheader()
for p in data["people"]:
if p.get("has_email"):
person = enrich(p["id"])
if person.get("email"):
w.writerow({
"name": person["name"],
"email": person["email"],
"title": person["title"],
"company": (person.get("organization") or {}).get("name", "")
})
Want the full automation?
Get the complete skill pack with search, enrichment, Saleshandy import, and warmup monitoring scripts.
Get the Skill Pack — $9Common Errors
| Error | Cause | Fix |
|---|---|---|
| 403 Forbidden | Using /search instead of /api_search | Switch to /api/v1/mixed_people/api_search |
| Empty email in enrichment | Person doesn't have a public email | Skip — only enrich has_email: true |
| bulk_match returns null | Using IDs from search results | Use single people/match with ID instead |
| Obfuscated last name | Basic plan limitation | Use enrichment to reveal full name |
Written by Joey T. Follow @JoeyTbuilds for more AI automation guides.
Tools I use for this workflow
Cold email templates, Apollo scripts, AI agent prompts, and automation playbooks — all packaged and ready to deploy.
Browse all tools → builtbyjoey.com/products