Email verification in Python

Python reaches the ZapBounce API over plain HTTP, so requests is all you need and there is no SDK to keep current. The examples below run against the live API with a key in the environment.

One habit worth keeping from the start: use a Session. It pools the TCP connection, which turns a loop of ten thousand single verifications from a connection-per-call into something that finishes this afternoon.

requests.Session for pooling, urllib3.Retry mounted on the adapter so 429 and 5xx back off without a hand-rolled loop.

Install

Shell
pip install requests

Verify one address

You branch on four results: valid, invalid, catch_all and unknown. Role and disposable are boolean flags beside the result, so test them separately. Unknown is the branch most code forgets, and it is never billed.

Python
import os
import requests

session = requests.Session()
session.headers.update({"Authorization": f"Bearer {os.environ['ZAPBOUNCE_KEY']}"})

def verify(email: str) -> dict:
    r = session.post(
        "https://api.zapbounce.com/v1/verify",
        json={"email": email},
        timeout=(3.05, 10),   # connect, read — the read side is the slow mail server
    )
    r.raise_for_status()
    return r.json()

result = verify("ada@example.com")

if result["result"] == "valid":
    send_to(result["email"])
elif result["result"] == "catch_all":
    queue_for_separate_send(result["email"])   # the domain accepts everything
elif result["result"] == "unknown":
    keep_and_retry_later(result["email"])      # not billed, not a verdict
else:
    suppress(result["email"])

Verify a list

Submit a batch rather than looping the single endpoint. We pace probes per receiving mail host, which protects the sending reputation your results depend on.

Python
from typing import Iterator

def submit(emails: list[str], name: str) -> str:
    r = session.post(
        "https://api.zapbounce.com/v1/batches",
        json={"name": name, "emails": emails},
        timeout=(3.05, 30),
    )
    r.raise_for_status()
    return r.json()["batch_id"]

def results(batch_id: str) -> Iterator[dict]:
    """Yield every row, following the cursor. Never build the full list in memory."""
    cursor = None
    while True:
        params = {"limit": 1000}
        if cursor:
            params["cursor"] = cursor
        page = session.get(
            f"https://api.zapbounce.com/v1/batches/{batch_id}/results",
            params=params, timeout=(3.05, 30),
        ).json()
        yield from page["data"]
        if not page["has_more"]:
            return
        cursor = page["next_cursor"]

batch_id = submit(load_emails("contacts.csv"), name="q4-webinar")
# ... wait for the webhook, then:
billed = sum(1 for row in results(batch_id) if row["billed"])
print(f"{billed} credits spent; the rest came back unknown and cost nothing")

Errors and retries

A 429 and a 5xx are worth retrying. A 402 for credits and a 400 for a malformed address will fail the same way on every attempt, so stop.

Python
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

# Retry-After on a 429 is honored by urllib3 itself, so no hand-rolled sleep loop.
retry = Retry(
    total=5,
    backoff_factor=0.5,
    status_forcelist=[429, 500, 502, 503, 504],
    allowed_methods=["GET", "POST"],
    respect_retry_after_header=True,
)
session.mount("https://", HTTPAdapter(max_retries=retry, pool_maxsize=20))

try:
    result = verify(email)
except requests.HTTPError as exc:
    body = exc.response.json().get("error", {})
    if body.get("code") == "insufficient_credits":
        raise OutOfCredits(body["message"]) from exc      # retrying will not help
    if exc.response.status_code == 402:
        raise
    logger.warning("verify failed request_id=%s", body.get("request_id"))
    raise
except requests.Timeout:
    # A verifier timing out is not a reason to block a signup. Fail open.
    result = {"email": email, "result": "unknown", "billed": False}

Python: common questions

Is there an official Python SDK?

No, and one would add a dependency to wrap four HTTP calls. requests plus the retry adapter above is the whole integration.

How do I verify at signup without blocking the form?

Give the call a two-second read timeout and treat a timeout as a pass. A verifier having a slow second should not cost you a registration.

Does async help?

For bulk, no: submit a batch and let us pace the probes. For a page that checks several addresses at once, httpx.AsyncClient with the same headers works the same way.

Run this Python code today

100 free checks a month, no card, credits that never expire, and unknown results that cost nothing.