Email Validation API in Python: Complete Guide

Use AI to summarize this article and ask questions

Grant Ammons
Grant Ammons – Founder April 18, 2026

Email Validation API in Python: Complete Guide

Learn how to validate email addresses in Python using the Truelist API. Step-by-step tutorial with code examples for requests, async, and batch validation.

TL;DR: Use the Truelist API to validate emails in Python with a single POST request to https://api.truelist.io/api/v1/verify. Works with requests for simple scripts, aiohttp for async applications, and supports batch validation with concurrency control. Pass your API key in the Authorization header and get back status, deliverability, and risk details in JSON.

Email validation in Python lets you programmatically verify whether an email address is real, deliverable, and safe to send to — going far beyond what regex patterns can tell you. Whether you’re building a Django web app, a Flask API, a data pipeline, or a simple cleanup script, integrating email validation at the API level gives you real-time insight into every address in your system.

This guide walks through integrating the Truelist email validation API into Python applications using both synchronous and asynchronous patterns, with practical code you can adapt to your stack.

Why Validate Emails in Python?

Python is the backbone of data pipelines, backend APIs, automation scripts, and machine learning workflows. If any of these systems touch email addresses, you need validation.

Here’s what happens when you skip it:

  • Hard bounces damage your sender reputation with every send
  • Spam traps get your domain blacklisted across major ISPs
  • Disposable emails bloat your list with users who’ll never engage
  • Invalid addresses waste resources on messages that will never arrive
  • Bad data pollutes your analytics and skews every downstream metric

Regex catches formatting issues — missing @ signs, spaces, invalid characters. But it can’t tell you whether john@company.com actually has a working mailbox. That requires DNS lookups, SMTP verification, and disposable email detection — exactly what an API does.

Getting Started with the Truelist API

You’ll need a Truelist API key before writing any code.

  1. Create an account at truelist.io
  2. Go to your dashboard and find the API section
  3. Generate your API key
  4. Store it in an environment variable — never hardcode it in your source files

The verification endpoint is:

POST https://api.truelist.io/api/v1/verify

Install the requests library if you haven’t already:

pip install requests

For the full list of parameters and response fields, see the API documentation.

Single Email Validation with Requests

Here’s a clean, production-ready function for validating a single email:

import os
import requests

TRUELIST_API_KEY = os.environ["TRUELIST_API_KEY"]
TRUELIST_API_URL = "https://api.truelist.io/api/v1/verify"


def validate_email(email: str) -> dict:
    """Validate a single email address using the Truelist API."""
    response = requests.post(
        TRUELIST_API_URL,
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {TRUELIST_API_KEY}",
        },
        json={"email": email},
        timeout=10,
    )
    response.raise_for_status()
    return response.json()


if __name__ == "__main__":
    result = validate_email("test@example.com")
    print(f"Status: {result['status']}")

    if result["status"] == "valid":
        print("Email is deliverable")
    elif result["status"] == "invalid":
        print("Email is not deliverable — do not send")
    else:
        print(f"Status is {result['status']} — proceed with caution")

Set your API key and run it:

export TRUELIST_API_KEY=your_api_key_here
python validate.py

The timeout=10 parameter prevents your script from hanging if the API is slow. The raise_for_status() call throws an exception on HTTP errors so you catch problems immediately.

Async Validation with aiohttp

For web applications built on async frameworks (FastAPI, aiohttp, Sanic) or for high-throughput scripts, async validation is significantly faster. Here’s the same functionality using aiohttp:

import os
import asyncio
import aiohttp

TRUELIST_API_KEY = os.environ["TRUELIST_API_KEY"]
TRUELIST_API_URL = "https://api.truelist.io/api/v1/verify"


async def validate_email_async(
    session: aiohttp.ClientSession, email: str
) -> dict:
    """Validate a single email using an async HTTP session."""
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {TRUELIST_API_KEY}",
    }
    async with session.post(
        TRUELIST_API_URL,
        headers=headers,
        json={"email": email},
        timeout=aiohttp.ClientTimeout(total=10),
    ) as response:
        if response.status != 200:
            text = await response.text()
            raise Exception(f"API error {response.status}: {text}")
        return await response.json()


async def main():
    async with aiohttp.ClientSession() as session:
        result = await validate_email_async(session, "test@example.com")
        print(f"Status: {result['status']}")


if __name__ == "__main__":
    asyncio.run(main())

Install aiohttp first:

pip install aiohttp

The key advantage here is connection reuse. The aiohttp.ClientSession maintains a connection pool, so subsequent requests skip the TCP/TLS handshake overhead.

Batch Validation

When you need to validate thousands of emails — cleaning a CSV export, processing a database migration, or running a scheduled list hygiene job — batch processing with concurrency control is essential:

import os
import asyncio
import aiohttp

TRUELIST_API_KEY = os.environ["TRUELIST_API_KEY"]
TRUELIST_API_URL = "https://api.truelist.io/api/v1/verify"


async def validate_email(
    session: aiohttp.ClientSession,
    email: str,
    semaphore: asyncio.Semaphore,
) -> dict:
    """Validate a single email with concurrency control."""
    async with semaphore:
        try:
            headers = {
                "Content-Type": "application/json",
                "Authorization": f"Bearer {TRUELIST_API_KEY}",
            }
            async with session.post(
                TRUELIST_API_URL,
                headers=headers,
                json={"email": email},
                timeout=aiohttp.ClientTimeout(total=10),
            ) as response:
                if response.status == 429:
                    retry_after = int(
                        response.headers.get("Retry-After", 2)
                    )
                    await asyncio.sleep(retry_after)
                    return await validate_email(
                        session, email, semaphore
                    )

                if response.status != 200:
                    return {"email": email, "status": "error"}

                data = await response.json()
                return {"email": email, **data}
        except Exception as e:
            return {"email": email, "status": "error", "error": str(e)}


async def validate_batch(
    emails: list[str], concurrency: int = 10
) -> list[dict]:
    """Validate a list of emails with bounded concurrency."""
    semaphore = asyncio.Semaphore(concurrency)
    async with aiohttp.ClientSession() as session:
        tasks = [
            validate_email(session, email, semaphore)
            for email in emails
        ]
        return await asyncio.gather(*tasks)


async def main():
    emails = [
        "valid@gmail.com",
        "invalid@nonexistentdomain.xyz",
        "disposable@tempmail.com",
        "typo@gmial.com",
        "real-user@company.com",
    ]

    results = await validate_batch(emails, concurrency=5)

    valid = [r for r in results if r.get("status") == "valid"]
    invalid = [r for r in results if r.get("status") == "invalid"]
    errors = [r for r in results if r.get("status") == "error"]

    print(f"Valid: {len(valid)}")
    print(f"Invalid: {len(invalid)}")
    print(f"Errors: {len(errors)}")

    for r in invalid:
        print(f"  - {r['email']}: {r.get('reason', 'unknown')}")


if __name__ == "__main__":
    asyncio.run(main())

The asyncio.Semaphore limits how many requests are in flight at once. Set it to 5-10 to stay well within API rate limits while still being much faster than sequential processing.

For very large lists (100k+ emails), consider using bulk email verification through the Truelist dashboard instead, which handles queuing, retries, and results delivery for you.

Handling API Responses

The Truelist API returns structured JSON with everything you need to decide what to do with an email. Here’s a practical response handler:

def categorize_email(result: dict) -> dict:
    """Categorize an email based on validation results."""
    status = result.get("status")
    is_disposable = result.get("is_disposable", False)
    is_role_based = result.get("is_role_based", False)

    # Hard reject — mailbox does not exist
    if status == "invalid":
        return {
            "action": "reject",
            "reason": result.get("reason", "Invalid email address"),
        }

    # Disposable/throwaway email
    if is_disposable:
        return {
            "action": "reject",
            "reason": "Disposable email address",
        }

    # Role-based (info@, support@, admin@) — higher bounce risk
    if is_role_based:
        return {
            "action": "flag",
            "reason": "Role-based address — may have lower engagement",
        }

    # Valid and safe
    if status == "valid":
        return {
            "action": "accept",
            "reason": "Email is valid and deliverable",
        }

    # Catch-all or unknown — accept but track
    return {
        "action": "accept_with_caution",
        "reason": f"Status: {status} — monitor for bounces",
    }

The key response fields:

  • statusvalid, invalid, unknown, or catch-all
  • reason — human-readable explanation for invalid addresses
  • is_disposableTrue if the email uses a throwaway service like Mailinator or Guerrilla Mail
  • is_role_basedTrue for generic addresses like info@, admin@, or support@

Error Handling

Production code needs to handle failures without crashing. Here’s a robust wrapper with retries and proper exception handling:

import time
import requests

TRUELIST_API_KEY = os.environ["TRUELIST_API_KEY"]
TRUELIST_API_URL = "https://api.truelist.io/api/v1/verify"


def validate_with_retry(
    email: str, max_retries: int = 3
) -> dict:
    """Validate an email with retry logic and exponential backoff."""
    for attempt in range(1, max_retries + 1):
        try:
            response = requests.post(
                TRUELIST_API_URL,
                headers={
                    "Content-Type": "application/json",
                    "Authorization": f"Bearer {TRUELIST_API_KEY}",
                },
                json={"email": email},
                timeout=10,
            )

            # Rate limited — back off and retry
            if response.status_code == 429:
                retry_after = int(
                    response.headers.get("Retry-After", 2)
                )
                time.sleep(retry_after)
                continue

            response.raise_for_status()
            return response.json()

        except requests.exceptions.Timeout:
            if attempt == max_retries:
                return {"status": "unknown", "error": "Request timed out"}
            time.sleep(2 ** attempt)

        except requests.exceptions.ConnectionError:
            if attempt == max_retries:
                return {"status": "unknown", "error": "Connection failed"}
            time.sleep(2 ** attempt)

        except requests.exceptions.HTTPError as e:
            if attempt == max_retries:
                return {"status": "unknown", "error": str(e)}
            time.sleep(2 ** attempt)

    return {"status": "unknown", "error": "Max retries exceeded"}

This handles the most common failure modes:

  1. Timeouts — the 10-second timeout prevents indefinite hangs
  2. Rate limiting (429) — respects the Retry-After header before retrying
  3. Connection errors — retries with exponential backoff (2s, 4s, 8s)
  4. HTTP errors — catches server errors and retries before giving up

Best Practices for Email Validation in Python

Validate at the point of collection. Whether it’s a web form, an API endpoint, or a CSV import script, validate emails the moment they enter your system. Catching bad addresses early is always cheaper than dealing with bounces later.

Use environment variables for your API key. Never hardcode credentials. Use os.environ or a library like python-dotenv to load configuration. This keeps secrets out of your codebase and makes deployment easier.

Reuse HTTP sessions. Both requests.Session() and aiohttp.ClientSession() maintain connection pools. Reusing sessions across multiple validation calls avoids the overhead of establishing new TCP connections for each request.

Set timeouts on every request. A missing timeout means your script can hang indefinitely if the API is unresponsive. Always set an explicit timeout — 10 seconds is a reasonable default for validation calls.

Fail open in user-facing applications. If the validation API is temporarily unavailable, don’t block user signups or form submissions. Accept the email and validate it asynchronously later. Losing a customer because your validation provider had a blip is worse than accepting one bad email.

Run periodic list hygiene. Email addresses decay over time — people leave companies, abandon accounts, and domains expire. A one-time validation isn’t enough. Schedule regular re-validation of your entire list to catch addresses that have gone bad since the last check.

Handle catch-all domains carefully. Some domains accept mail for any address, making it impossible to verify individual mailboxes. Track catch-all results separately and monitor them for bounces after sending.

Log validation results. Store the validation status alongside each email in your database. This lets you filter lists by quality, debug deliverability issues, and build reports on data quality over time.


Stop validating once and hoping for the best. Truelist’s recurring validation automatically re-checks your lists on a schedule — catching new bounces, dead mailboxes, and risky addresses before they damage your sender reputation. No credits, no per-email charges.

Set up recurring validation →

Ready to put Truelist
to the test?

Find out if Truelist is right for you in under 10 minutes.

Free plan available. No credit card required.