qodev-apollo-api
Async Python client for Apollo.io CRM API with full type safety.
Features
- Async-first design with httpx
- Full Pydantic v2 models for type safety
- Context manager support for clean resource management
- Intelligent contact matching with 3-tier fallback strategy
- Built-in rate limit tracking (400/hour, 200/min, 2000/day)
- 40+ API methods across 8 endpoint groups
- Comprehensive error handling with custom exceptions
- ProseMirror to Markdown conversion for notes
Installation
pip install qodev-apollo-api
Or with uv:
uv add qodev-apollo-api
Quick Start
from qodev_apollo_api import ApolloClient
async with ApolloClient() as client:
# Search contacts
contacts = await client.search_contacts(limit=10)
for contact in contacts.items:
print(f"{contact.name} - {contact.email}")
# Get contact details
contact = await client.get_contact("contact_id")
print(f"Title: {contact.title} at {contact.company}")
# Enrich organization data
company = await client.enrich_organization("apollo.io")
print(f"Employees: {company.get('estimated_num_employees')}")
# Create a note
await client.create_note(
content="Great conversation about Q1 goals",
contact_ids=["contact_id"],
)
Configuration
API Key
Set environment variable:
export APOLLO_API_KEY="your_api_key"
Or pass directly:
async with ApolloClient(api_key="your_api_key") as client:
...
Timeout
Customize request timeout (default 30 seconds):
async with ApolloClient(timeout=60.0) as client:
...
Rate Limiting
Apollo.io enforces these limits:
- 400 requests/hour (primary bottleneck for sustained operations)
- 200 requests/minute
- 2,000 requests/day
Monitor rate limits via client.rate_limit_status:
async with ApolloClient() as client:
await client.search_contacts(limit=10)
status = client.rate_limit_status
print(f"Hourly: {status['hourly_left']}/{status['hourly_limit']}")
print(f"Minute: {status['minute_left']}/{status['minute_limit']}")
print(f"Daily: {status['daily_left']}/{status['daily_limit']}")
Best practices:
- Add delays between requests (10+ seconds for sustained operations)
- Monitor
hourly_left- stop if < 50 requests remaining - Handle
RateLimitErrorwith exponential backoff
Contact Matching
Three-tier fallback strategy for robust contact finding:
contact_id = await client.find_contact_by_linkedin_url(
linkedin_url="https://linkedin.com/in/johndoe",
person_name="John Doe", # Fallback if URL changed
create_if_missing=True, # Auto-create from People DB (210M+ contacts)
contact_stage_id="stage_id", # Stage to assign if created
)
How it works:
- LinkedIn URL search - Exact match (most reliable)
- Name search - Handles URL changes (requires unique match)
- People database - Auto-create if enabled (verifies URL match)
Common scenarios:
- LinkedIn URLs change when users update custom URLs
- Name search returns multiple matches → skipped for safety
- People database doesn't support special characters (umlauts, etc.)
API Methods
Contacts
# Search
contacts = await client.search_contacts(
limit=100,
q_keywords="CEO",
contact_stage_ids=["stage_id"],
)
# Get by ID
contact = await client.get_contact("contact_id")
# Create
result = await client.create_contact(
first_name="John",
last_name="Doe",
email="john@example.com",
title="CEO",
)
# Get contact stages
stages = await client.get_contact_stages()
# Find by LinkedIn URL (3-tier fallback)
contact_id = await client.find_contact_by_linkedin_url(
linkedin_url="https://linkedin.com/in/johndoe",
person_name="John Doe",
)
Accounts
# Search
accounts = await client.search_accounts(
limit=100,
q_organization_name="Apollo",
)
# Get by ID
account = await client.get_account("account_id")
Deals / Opportunities
# Search
deals = await client.search_deals(
limit=100,
opportunity_stage_ids=["stage_id"],
)
# Get by ID
deal = await client.get_deal("deal_id")
Pipelines & Stages
# List all pipelines
pipelines = await client.list_pipelines()
# Get pipeline by ID
pipeline = await client.get_pipeline("pipeline_id")
# List stages for pipeline
stages = await client.list_pipeline_stages("pipeline_id")
Enrichment
# Enrich organization (35M+ companies, free)
company = await client.enrich_organization("apollo.io")
# Enrich person (210M+ people, costs 1 credit)
person = await client.enrich_person("john@example.com")
# Search people database (free)
results = await client.search_people(q_keywords="CEO Apollo")
Notes
# Search notes
notes = await client.search_notes(
contact_ids=["contact_id"],
limit=50,
)
# Create note
result = await client.create_note(
content="Meeting notes from Q1 planning",
contact_ids=["contact_id"],
account_ids=["account_id"],
)
Activities
# Search calls, tasks, emails
calls = await client.search_calls(limit=100)
tasks = await client.search_tasks(limit=100)
emails = await client.search_emails(limit=100)
# Create task
result = await client.create_task(
contact_ids=["contact_id"],
note="Follow up on proposal",
priority="high",
)
# List contact activities
calls = await client.list_contact_calls("contact_id")
tasks = await client.list_contact_tasks("contact_id")
News & Jobs
# Get news for account
news = await client.list_account_news("account_id")
# Get job postings for account
jobs = await client.list_account_jobs("account_id")
Models
All responses are typed Pydantic models:
- Contact - All contact fields (id, name, email, title, linkedin_url, phone_numbers, etc.)
- Account - All account fields (id, name, domain, employees, revenue, industries, tech stack, etc.)
- Deal - All deal fields (id, name, amount, stage, close_date, is_won, etc.)
- Pipeline - Pipeline info (id, title, is_default, sync_enabled, etc.)
- Stage - Stage info (id, name, probability, is_won, is_closed, etc.)
- Note - Notes with Markdown content (converted from ProseMirror JSON)
- Call, Task, Email - Activity records
- EmploymentHistory - Work history entries
- PaginatedResponse[T] - Generic pagination wrapper
Error Handling
from qodev_apollo_api import ApolloClient, AuthenticationError, RateLimitError, APIError
try:
async with ApolloClient() as client:
contacts = await client.search_contacts(limit=10)
except AuthenticationError:
print("Invalid API key")
except RateLimitError as e:
print(f"Rate limit exceeded. Retry after {e.retry_after} seconds")
except APIError as e:
print(f"API error: {e} (status: {e.status_code})")
Development
# Install dependencies
make install
# Install pre-commit hooks
make install-hooks
# Run all checks (lint, format, typecheck, typos)
make check
# Run tests with coverage
make test
# Run tests without coverage
make test-fast
# Lint and format code
make lint
make format
# Type checking
make typecheck
# Spell check
make typos
# Clean generated files
make clean
License
MIT
Metadata
Release files for qodev-apollo-api 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| qodev_apollo_api-0.6.0.tar.gz | 93.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| qodev_apollo_api-0.6.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 125.5 kB
Release files / qodev_apollo_api-0.6.0.tar.gz
| Download URL | qodev_apollo_api-0.6.0.tar.gz |
|---|---|
| Size | 93.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
07052b984f817f52a7702a5bf0028dbe929fd78cc18ad29b73da30cf0dbeaf7e
|
|
BLAKE2b-256 checksum How to use checksums |
f7e4b2dfd65d05677219a91883047c1754040363781268f1c5063a0a37a8bde5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 31, 2026.
Transparency logRelease files / qodev_apollo_api-0.6.0-py3-none-any.whl
| Download URL | qodev_apollo_api-0.6.0-py3-none-any.whl |
|---|---|
| Size | 31.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
641abce5b318c140a988b9c361e4df6930dc149d1cbb5292e4f4ae662c6f6fc6
|
|
BLAKE2b-256 checksum How to use checksums |
524a93338e854fd9efbfb45854ad883b972653883da703b38bcf9ac62c847453
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 31, 2026.
Transparency log