Overview
We've released the official FoxReach Python SDK - a typed, ergonomic client for the FoxReach API. Whether you're building a custom CRM integration, automating lead imports, or scripting campaign management, the SDK handles authentication, pagination, retries, and error handling so you can focus on your logic. It is one of the official clients on the integrations page, and it is the engine underneath the FoxReach CLI.
Install it with pip (Python 3.9 or newer; the only runtime dependency is httpx):
pip install foxreach
The current release on PyPI is 0.2.0.
Quick Start
The client needs a workspace API key. Keys start with otr_ and are created in the dashboard under Settings > Integrations > API Keys; the API keys and webhooks guide shows how to create one and which scopes to give it. The SDK sends the key in the X-API-Key header on every request to https://api.foxreach.io/api/v1 and raises ValueError at construction time if the key does not start with otr_.
from foxreach import FoxReach
client = FoxReach(api_key="otr_your_key")
# List active leads (first page)
leads = client.leads.list(status="active")
for lead in leads:
print(f"{lead.email} - {lead.company}")
# Create a new lead
from foxreach import LeadCreate
lead = client.leads.create(LeadCreate(
email="jane@example.com",
first_name="Jane",
last_name="Smith",
company="TechCorp",
))
print(f"Created lead: {lead.id}")
What's Included
The SDK covers the v1 API resources:
- Leads -
client.leads.list(),create(),get(),update(),delete(),activity() - Campaigns - Full lifecycle:
create(),update(),start(),pause(),resume(),delete(), plusadd_leads(),add_accounts(),remove_lead(),remove_account() - Sequences -
client.campaigns.sequences.list(),create(),update(),delete() - Templates -
client.templates.list(),get(),create(),update(),delete() - Email Accounts -
client.email_accounts.list(),get(),delete() - Inbox -
client.inbox.list_threads(),get(),update(),get_conversation(),send_reply(),stats() - Analytics -
client.analytics.overview(),campaign() - Webhooks -
client.webhooks.list_events(),list(),create(),update(),delete()
Typed Models
Every request and response uses typed dataclasses. Your IDE gives you autocomplete and type checking out of the box:
from foxreach import CampaignCreate
campaign = client.campaigns.create(CampaignCreate(
name="Q1 Enterprise Outreach",
timezone="America/New_York",
sending_days=[1, 2, 3, 4, 5],
sending_start_hour=9,
sending_end_hour=17,
daily_limit=50,
))
Field names are snake_case in Python even though the API itself is camelCase. The HTTP layer converts outgoing bodies to camelCase and incoming keys back to snake_case, so daily_limit here becomes dailyLimit on the wire and comes back as campaign.daily_limit.
Pagination
List endpoints return one page at a time as a PaginatedResponse. Iterating the response yields the items on that page, and meta tells you where you are:
page = client.leads.list(page=1, page_size=50)
print(f"Total: {page.meta.total}, Page: {page.meta.page} of {page.meta.total_pages}")
for lead in page.data:
print(lead.email)
if page.has_next_page():
page = page.next_page()
To walk every page without managing page numbers yourself, use auto_paging_iter(). It fetches the next page on demand as you consume the iterator:
# Fetches page after page until the list is exhausted
for lead in client.leads.list(status="active").auto_paging_iter():
process_lead(lead)
page_size accepts up to 100.
Lead Tags
Leads carry a list of tags, which is the natural way to group an imported list. Pass tags on create or update, and filter with a comma-separated tags string on list (a lead matches if it has any of the named tags):
from foxreach import LeadCreate, LeadUpdate
client.leads.create(LeadCreate(email="jane@example.com", tags=["dental", "q3-list"]))
client.leads.update("cld_abc123", LeadUpdate(tags=["dental", "warm"]))
for lead in client.leads.list(tags="dental,warm").auto_paging_iter():
print(lead.email, lead.tags)
Omitting tags on an update leaves the current set untouched; passing an empty list clears it.
Inbox
The inbox resource reads replies and answers them from the same account and thread the lead wrote to:
from foxreach import ThreadUpdate
# Unread replies that the categorizer marked interested
threads = client.inbox.list_threads(category="interested", is_read=False)
for t in threads:
print(t.from_email, t.subject)
# The whole back-and-forth with that lead
conversation = client.inbox.get_conversation("rpl_abc123")
# Mark as read, then reply in-thread
client.inbox.update("rpl_abc123", ThreadUpdate(is_read=True))
client.inbox.send_reply("rpl_abc123", "Thanks Jane, does Thursday at 10am work?")
# Counts by category
print(client.inbox.stats())
list_threads() also filters by campaign_id, account_id, is_starred, and search.
Webhooks
You can subscribe a URL to FoxReach events from code, which is what the n8n trigger node and the Zapier and Make apps do behind the scenes:
from foxreach import WebhookCreate
print(client.webhooks.list_events())
# ['email.sent', 'email.failed', 'email.bounced', ..., 'reply.received', 'reply.categorized', ...]
webhook = client.webhooks.create(WebhookCreate(
url="https://example.com/hooks/foxreach",
events=["reply.received", "reply.categorized", "email.bounced"],
))
print(webhook.secret) # returned once, on create only
Store the secret: every delivery carries an X-Webhook-Signature header with the HMAC-SHA256 hex digest of the raw request body, and the secret is not returned by list() or update() later. webhook.consecutive_failures tells you when an endpoint has been failing.
Email Accounts and Analytics
Before assigning accounts to a campaign, check their health and remaining daily capacity; then read the numbers back once it is running:
for account in client.email_accounts.list():
print(account.email, account.health_score, account.sent_today, "/", account.daily_limit)
overview = client.analytics.overview()
print(f"Reply rate: {overview.reply_rate}%")
stats = client.analytics.campaign("cmp_xyz")
print(f"Sent: {stats.sent}, Replied: {stats.replied}, Bounced: {stats.bounced}")
for day in stats.daily_stats:
print(day.date, day.sent, day.replied)
Async Support
The SDK ships with a fully async client for use with asyncio. It has the same resources and methods, awaited:
from foxreach import AsyncFoxReach
async with AsyncFoxReach(api_key="otr_your_key") as client:
leads = await client.leads.list(status="active")
for lead in leads:
print(lead.email)
# Or walk every page
async for lead in (await client.leads.list()).auto_paging_iter():
print(lead.email)
The async with block closes the underlying connection pool for you. Outside a context manager, call await client.close() when you are done; the sync client has client.close() and also works as a regular context manager.
Error Handling
API errors are raised as typed exceptions. Every one carries status_code and response_body, and str(e) is the API's message:
from foxreach import FoxReachError, NotFoundError, ValidationError, RateLimitError, AuthenticationError
try:
lead = client.leads.get("cld_nonexistent")
except NotFoundError:
print("Lead not found")
except ValidationError as e:
print(f"Invalid request: {e}")
except RateLimitError as e:
print(f"Rate limited, retry after {e.retry_after}s")
except AuthenticationError:
print("Invalid or revoked API key")
except FoxReachError as e:
print(f"API error {e.status_code}: {e}")
The mapping is 400 BadRequestError, 401 AuthenticationError, 403 PermissionError (the key lacks the read or write scope the endpoint needs), 404 NotFoundError, 422 ValidationError, 5xx ServerError, and ConnectionError for network failures and timeouts.
The v1 API allows 100 requests per minute per key. On a 429 the SDK sleeps for the number of seconds in the Retry-After header and retries, up to max_retries times (default 3), before raising RateLimitError. You can tune that, the timeout, and the base URL on the client:
client = FoxReach(
api_key="otr_your_key",
base_url="https://api.foxreach.io/api/v1", # default
timeout=30.0, # seconds
max_retries=3, # retries on 429
)
End-to-End Example: Import Leads and Launch a Campaign
Here's a complete script that creates leads, builds a campaign with a sequence, and starts it:
from foxreach import FoxReach, LeadCreate, CampaignCreate, SequenceCreate
client = FoxReach(api_key="otr_your_key")
# 1. Create leads
leads = []
contacts = [
("sarah@techcorp.io", "Sarah", "Chen", "TechCorp"),
("mike@startupco.com", "Mike", "Johnson", "StartupCo"),
("lisa@enterprise.com", "Lisa", "Wang", "Enterprise Ltd"),
]
for email, first, last, company in contacts:
lead = client.leads.create(LeadCreate(
email=email,
first_name=first,
last_name=last,
company=company,
tags=["q1-outreach"],
))
leads.append(lead)
print(f"Created lead: {lead.email}")
# 2. Create a campaign
campaign = client.campaigns.create(CampaignCreate(
name="Q1 Outreach",
timezone="America/New_York",
sending_days=[1, 2, 3, 4, 5],
sending_start_hour=9,
sending_end_hour=17,
daily_limit=50,
))
# 3. Add sequence steps
client.campaigns.sequences.create(campaign.id, SequenceCreate(
subject="Quick question about {{company}}",
body="Hi {{firstName}},\n\nI noticed {{company}} is growing fast...",
delay_days=0,
))
client.campaigns.sequences.create(campaign.id, SequenceCreate(
subject="Re: Quick question about {{company}}",
body="Hi {{firstName}},\n\nJust following up...",
delay_days=3,
))
# 4. Add leads to campaign
lead_ids = [lead.id for lead in leads]
client.campaigns.add_leads(campaign.id, lead_ids)
# 5. Assign email account
accounts = client.email_accounts.list()
client.campaigns.add_accounts(campaign.id, [accounts.data[0].id])
# 6. Start the campaign
client.campaigns.start(campaign.id)
print(f"Campaign '{campaign.name}' is now active!")
client.close()


