Python SDK

The official Python SDK offers a synchronous and an asynchronous client with the same methods, typed results and typed errors. It needs Python 3.10 or later and uses httpx for HTTP.

Install

pip install email-validation-sdk

The package is imported as email_validation.

Validate an address

import os

from email_validation import EmailValidation

with EmailValidation(os.environ["EV_API_KEY"]) as ev:
    result = ev.validate("accepted@sandbox.email-validation.org")
    print(result["status"], result["reason"], result["action"])  # deliverable accepted send

Options: ip_address and timeout (seconds the check may take, 5–15). Results are plain dictionaries described by TypedDict types, so editors and type checkers know every field.

Async

import asyncio
import os

from email_validation import AsyncEmailValidation


async def main() -> None:
    async with AsyncEmailValidation(os.environ["EV_API_KEY"]) as ev:
        results = await asyncio.gather(
            ev.validate("accepted@sandbox.email-validation.org"),
            ev.validate("catch_all@sandbox.email-validation.org"),
        )
        for r in results:
            print(r["email"], r["status"])


asyncio.run(main())

Keep concurrency within your rate limit; for long lists use a bulk job.

Handle errors

from email_validation import EmailValidationError, InsufficientCreditsError, RateLimitError

try:
    ev.validate(address)
except InsufficientCreditsError:
    ...  # buy credits; nothing was charged
except RateLimitError as err:
    print("retry in", err.retry_after, "seconds")
except EmailValidationError as err:
    print(err.code, err.status, err.request_id)

Every error has status (0 when no answer arrived), code, request_id and retry_after. Classes: InvalidRequestError, AuthenticationError, InsufficientCreditsError, PermissionDeniedError, NotFoundError, ConflictError, RateLimitError, ServiceUnavailableError, ServerError and APIConnectionError.

Retries and timeouts

ev = EmailValidation(
    os.environ["EV_API_KEY"],
    timeout=30.0,  # seconds per attempt
    max_retries=2,  # after 429 / 503 answers
    max_retry_wait=10.0,  # longer Retry-After values are raised to you instead of waited out
)

429 and 503 answers are retried with backoff, honouring Retry-After. A daily key cap is never retried, and a validation is never retried after a network failure, because it may already have been charged.

Credits, usage and status

credits = ev.credits()  # {"balance": ..., "debt": ..., "pending": ..., "lots": [...]}
usage = ev.usage(from_date="2026-11-01", to_date="2026-11-30", group_by="day")
status = ev.status()

Bulk jobs

job = ev.bulk.create(emails=["accepted@sandbox.email-validation.org", "catch_all@sandbox.email-validation.org"])

with open("customers.csv", "rb") as f:
    job = ev.bulk.create(file=f.read(), column="email", has_header=True)

job = ev.bulk.wait(job["id"], on_progress=lambda j: print(j["status"], j["processed"]))
if job["status"] == "completed":
    csv_bytes = ev.bulk.results(job["id"], filter=["deliverable", "risky"])
elif job["status"] == "ready":
    print("could not start:", job["error_code"])  # e.g. insufficient_credits: buy credits, then ev.bulk.start()

wait() returns when the job is completed, failed, cancelled or expired, or when it is still ready with an error_code: it could not start, for example insufficient_credits, and does not move on by itself. Pass timeout= (seconds) to give up earlier.

Also: ev.bulk.get(job_id), ev.bulk.start(job_id) for jobs created with start=False (call it before wait()), and ev.bulk.cancel(job_id). results returns the whole file as bytes; for very large jobs, download the full file (filter="all", original columns) in parts with range="bytes=…". Other shapes cannot be fetched in parts and raise InvalidRequestError when given a range.

Verify webhooks

import os

from email_validation import WebhookVerificationError, verify_webhook
from flask import Flask, request

app = Flask(__name__)


@app.post("/webhooks/email-validation")
def webhook():
    try:
        event = verify_webhook(request.get_data(), request.headers, os.environ["EV_WEBHOOK_SECRET"])
    except WebhookVerificationError:
        return "invalid signature", 400
    if event["type"] == "bulk.completed":
        print("job done", event["data"]["job_id"])
    return "", 204

event["type"] can also be webhook.test, sent when you press Send test in the dashboard: answer it with a 2xx like any other event. sign_webhook creates a valid signature for local tests.

Bring your own HTTP client

Pass transport= to use a configured httpx.Client (proxies, custom TLS) through HttpxTransport(client), or any object with send(request) and close() methods, for example in tests.

Updated · Maintained by the email-validation.org team