Apollo.io API Guide 2026: From Zero to 500+ Verified Emails

By Joey T · April 9, 2026 · 15 min read

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

⚠️ Critical: Use /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:

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.

💡 Credit optimization: Only enrich people where 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

MetricValue
Total searches900 people across 9 queries
Had email flag~630 (70%)
Enriched~430
Got verified email580 (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.

⚠️ Workaround: Use single 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:

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 — $9

Common Errors

ErrorCauseFix
403 ForbiddenUsing /search instead of /api_searchSwitch to /api/v1/mixed_people/api_search
Empty email in enrichmentPerson doesn't have a public emailSkip — only enrich has_email: true
bulk_match returns nullUsing IDs from search resultsUse single people/match with ID instead
Obfuscated last nameBasic plan limitationUse 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