SDK8 min read

Python SDK quick start — first test in 10 minutes

pip install inbox-check, paste your API key, make one call. You have your first placement test running. Here is the full quick start, sync and async.

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.

What you need

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-check

The 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_xxxxxxxxxxxx

Or 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 return

Polling 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")
        break

Async 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())
Context manager is the right way

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 dict indexing (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.

Frequently asked questions

Does the SDK support the free tier?

Yes — the free tier uses the same endpoints. Sign up for a free API key, paste it into INBOX_CHECK_API_KEY, and you get 10 tests a month on the Developer-free plan.

What Python versions are supported?

3.9, 3.10, 3.11, 3.12, 3.13. 3.8 is end-of-life and no longer tested. Typed hints use syntax that requires 3.9+.

Is the SDK thread-safe?

Yes for the sync client (InboxCheck) — share a single instance across threads. The async client (AsyncInboxCheck) is designed for one event loop and should not be shared across loops.

How do I get raw JSON if the typed model misses a field?

Every response exposes a .model_extra attribute (from pydantic v2) with unknown fields preserved. Or call ic._raw.get('/api/check/{id}').json() for the untyped dict. But please open an issue if a field is missing from the model — we mirror the API spec.
Related reading

Check your deliverability across 20+ providers

Gmail, Outlook, Yahoo, Mail.ru, Yandex, GMX, ProtonMail and more. Real inbox screenshots, SPF/DKIM/DMARC, spam engine verdicts. Free, no signup.

Run Free Test →

Unlimited tests · 20+ seed mailboxes · Live results · No account required