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-sdkThe 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 sendOptions: 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 "", 204event["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.