The Inbox Check Python SDK gives you typed, idiomatic access to the placement API: sync and async clients, streaming results through an iterator, typed exceptions, and await() helper that handles polling for you. Ten minutes from nothing installed to a working test in a script or a Jupyter notebook.
Python 3.9+ and an Inbox Check API key (free tier: 10 tests a month, no credit card). That is it. No extra system deps, no compile step.
pip install
pip install inbox-check
# or, if you use Poetry:
poetry add inbox-check
# or, with the modern uv:
uv pip install inbox-checkThe package is published on PyPI under the name inbox-check. It pulls in httpx, pydantic v2 and nothing else.
Setting the API key
Two options. Environment variable (recommended for anything beyond a throwaway script):
export INBOX_CHECK_API_KEY=ic_live_xxxxxxxxxxxxOr pass it explicitly to the constructor. Useful for multi-tenant systems where you juggle keys per customer:
from inbox_check import InboxCheck
ic = InboxCheck(api_key="ic_live_xxxxxxxxxxxx")Client initialisation (sync)
The zero-argument form is the common case:
from inbox_check import InboxCheck
ic = InboxCheck() # reads INBOX_CHECK_API_KEY
# With explicit options:
ic = InboxCheck(
api_key="ic_live_xxxx",
base_url="https://check.live-direct-marketing.online",
timeout=30.0, # seconds; default 30
max_retries=3, # retries on 5xx; default 3
)Your first placement test
Three fields are required: a sender domain, a subject, and an HTML body. The SDK sends the request, parses the response into a typed Test model, and returns.
from inbox_check import InboxCheck
ic = InboxCheck()
test = ic.tests.create(
sender_domain="news.acme.io",
subject="Weekly digest",
html="<html><body><p>Hello from the monitor.</p></body></html>",
text="Hello from the monitor.", # optional but recommended
)
print(test.id) # "t_01H9X..."
print(test.status) # "queued" on returnPolling vs streaming
A test takes 30–120 seconds to complete depending on the seed pool's current load. Two ways to get the result:
Polling helper: ic.tests.wait()
The simplest option. Blocks until the test is complete and returns the full result:
result = ic.tests.wait(test.id) # polls every 3s, 5-min timeout
print(result.summary.inbox_rate) # 0.82
print(result.summary.inbox_count, "/", result.summary.total)
for p in result.providers:
print(f"{p.name:20} {p.folder}")Streaming: ic.tests.stream()
If you want per-seed events as they come in (a nice UX for a CLI or a dashboard), use the stream iterator. The SDK wraps the server's SSE feed into a regular Python iterable:
for event in ic.tests.stream(test.id):
if event.type == "placement":
p = event.data
print(f"[{p.name}] {p.folder}")
elif event.type == "complete":
final = event.data
print(f"Done: {final.summary.inbox_rate:.0%} inbox")
breakAsync client
For FastAPI, Starlette, asyncio workers, or any concurrent workload, use AsyncInboxCheck. The surface is identical, just await every call.
import asyncio
from inbox_check import AsyncInboxCheck
async def main():
async with AsyncInboxCheck() as ic:
test = await ic.tests.create(
sender_domain="news.acme.io",
subject="Weekly digest",
html="<p>Hello</p>",
)
result = await ic.tests.wait(test.id)
print(result.summary.inbox_rate)
# Streaming is an async iterator:
async for event in ic.tests.stream(test.id):
if event.type == "complete":
break
asyncio.run(main())Always wrap AsyncInboxCheck in an async with block (or call await ic.aclose() when you are done). The underlying httpx.AsyncClient holds connections open for reuse; leaking it will warn on process exit.
Handling errors
The SDK raises typed exceptions, so you can catch the specific failure modes without parsing strings:
from inbox_check import (
InboxCheck,
AuthError, # 401
RateLimitError, # 429 (includes retry_after)
ValidationError, # 400 (invalid payload)
NotFoundError, # 404
ServerError, # 5xx (after retries)
InboxCheckError, # base class
)
ic = InboxCheck()
try:
test = ic.tests.create(sender_domain="news.acme.io",
subject="x", html="<p>ok</p>")
result = ic.tests.wait(test.id, timeout=600)
except RateLimitError as e:
print(f"Backoff {e.retry_after}s, then retry")
except AuthError:
print("Bad or revoked API key")
except ValidationError as e:
print(f"Server said: {e.message}")
except InboxCheckError as e:
print(f"Something else went wrong: {e}")Testing from Jupyter
The SDK works inside Jupyter with no extra setup. Useful for ad-hoc diagnosis: paste a template, run a test, inspect the per-provider dataframe.
import pandas as pd
from inbox_check import InboxCheck
ic = InboxCheck()
test = ic.tests.create(
sender_domain="news.acme.io",
subject="Sanity check",
html=open("template.html").read(),
)
result = ic.tests.wait(test.id)
df = pd.DataFrame([p.model_dump() for p in result.providers])
df.groupby(["name", "folder"]).size().unstack(fill_value=0)A complete 40-line monitoring script
Puts it all together: load senders from a YAML file, run a test per sender, print a summary, exit non-zero on regression.
# monitor.py
import os, sys, yaml
from inbox_check import InboxCheck, InboxCheckError
ic = InboxCheck()
with open("senders.yml") as f:
senders = yaml.safe_load(f)["senders"]
failures = []
for s in senders:
try:
test = ic.tests.create(
sender_domain=s["domain"],
subject=s.get("subject", "Monitor"),
html=s.get("html", "<p>ok</p>"),
)
result = ic.tests.wait(test.id)
rate = result.summary.inbox_rate
status = "OK" if rate >= s.get("threshold", 0.8) else "FAIL"
print(f"{status:5} {s['domain']:30} {rate:>6.1%}")
if status == "FAIL":
failures.append(s["domain"])
except InboxCheckError as e:
print(f"ERR {s['domain']:30} {e}")
failures.append(s["domain"])
if failures:
print(f"\n{len(failures)} regression(s): {failures}")
sys.exit(1)Migrating from the raw requests approach
If you are on a pre-SDK script that calls the REST API withrequests directly, the migration is mechanical:
requests.post(...)→ic.tests.create(...). Keyword args replace the JSON body.- Polling loop with
while status != "complete"→ic.tests.wait(test_id). - JSON
dictindexing (r["summary"]["inboxCount"]) → typed attributes (result.summary.inbox_count). - Status-code branches →
except RateLimitError / AuthError / ....
A 60-line raw script typically collapses to 15–20 lines with the SDK.