Skip to main content

qodev-apollo-api

CI PyPI Python

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 RateLimitError with 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:

  1. LinkedIn URL search - Exact match (most reliable)
  2. Name search - Handles URL changes (requires unique match)
  3. 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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

qodev_apollo_api-0.5.0.tar.gz (88.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

qodev_apollo_api-0.5.0-py3-none-any.whl (29.6 kB view details)

Uploaded Python 3

File details

Details for the file qodev_apollo_api-0.5.0.tar.gz.

File metadata

  • Download URL: qodev_apollo_api-0.5.0.tar.gz
  • Upload date:
  • Size: 88.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for qodev_apollo_api-0.5.0.tar.gz
Algorithm Hash digest
SHA256 d31f63f1e9aa00b237506a98d9e9376015de197549bcb51ed8f1fc2d77e520ff
MD5 80f5bab6af87b3dd141866395b0f8e02
BLAKE2b-256 c085363d77f08c3c112388eb7677e0e4d70790830f9bb944c0a8d5998c6c282e

See more details on using hashes here.

Provenance

The following attestation bundles were made for qodev_apollo_api-0.5.0.tar.gz:

Publisher: publish.yml on qodevai/apollo-api

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file qodev_apollo_api-0.5.0-py3-none-any.whl.

File metadata

File hashes

Hashes for qodev_apollo_api-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 17a40a92604905f59c0c48deb020685898802c3f635fe0abab8cd3d2a7b2aca2
MD5 ec3e40c8cc8dc078a8b7f513f614ff30
BLAKE2b-256 2c45950a3ad841f9dcb3646e738133d1584fe01031cec100a87a704b82136b5f

See more details on using hashes here.

Provenance

The following attestation bundles were made for qodev_apollo_api-0.5.0-py3-none-any.whl:

Publisher: publish.yml on qodevai/apollo-api

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 files

0.4.0

2 files

0.3.2

2 files

0.3.1

2 files

0.3.0

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page