# Python SDK

URL: https://email-validation.org/docs/sdks/python/
Updated: 2026-10-20

> Documentation index: https://email-validation.org/llms.txt

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](https://www.python-httpx.org/) for HTTP.

## Install

```bash
pip install email-validation-sdk
```

The package is imported as `email_validation`.

## Validate an address

```python
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

```python
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](https://email-validation.org/docs/rate-limits/); for long lists use a bulk job.

## Handle errors

```python
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

```python
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

```python
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

```python
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

```python
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.
