Email Validation

2 endpoints for thorough email validation — beyond syntax checking to live MX resolution and SMTP mailbox probing.

Method Endpoint Purpose
GET /v1/email/validate Full validation pipeline for a single address
POST /v1/email/validate-batch Validate up to 50 addresses in one request

Python SDK Examples

Validate a single address

validate runs the full pipeline: RFC 5322 syntax check → MX resolution → SMTP mailbox probe → disposable/free/role-account flags.

from toolkitapi import Email

with Email(api_key="tk_...") as email:
    result = email.validate("[email protected]")

    print(result["deliverability"])   # "deliverable" | "undeliverable" | "risky" | "unknown"
    print(result["confidence"])       # 0.0 – 1.0 confidence in the verdict
    print(result["syntax_valid"])     # True
    print(result["mx_found"])         # True
    print(result["mx_records"])       # ["mail.toolkitapi.io"]
    print(result["is_deliverable"])   # True (SMTP verdict)
    print(result["is_disposable"])    # False
    print(result["is_free"])          # False (gmail.com etc.)
    print(result["is_role_account"])  # False (support@, noreply@, etc.)
    print(result["smtp_code"])        # 250

Gate a sign-up form

from toolkitapi import Email

def is_acceptable_signup_email(address: str) -> tuple[bool, str]:
    """Return (ok, reason). Reject disposable and role accounts."""
    with Email(api_key="tk_...") as email:
        result = email.validate(address)

    if not result["syntax_valid"]:
        return False, "Invalid email address format."
    if not result["mx_found"]:
        return False, "Email domain does not appear to accept mail."
    if result["is_disposable"]:
        return False, "Disposable email addresses are not accepted."
    if result["is_role_account"]:
        return False, "Please use a personal email address, not a group inbox."
    if result["deliverability"] == "undeliverable":
        return False, "This email address does not appear to exist."
    return True, ""

ok, reason = is_acceptable_signup_email("[email protected]")
print(ok, reason)  # False  Disposable email addresses are not accepted.

Batch validation

validate_batch validates up to 50 addresses concurrently with controlled SMTP concurrency. Returns per-address results and a summary.

from toolkitapi import Email

addresses = [
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "totally-invalid",
]

with Email(api_key="tk_...") as email:
    response = email.validate_batch(addresses)

print(f"Deliverable:   {response['summary']['deliverable']}")
print(f"Undeliverable: {response['summary']['undeliverable']}")
print(f"Risky:         {response['summary']['risky']}")
print(f"Unknown:       {response['summary']['unknown']}")

for r in response["results"]:
    status = r["deliverability"].upper()
    disp = " [disposable]" if r["is_disposable"] else ""
    role = " [role]" if r["is_role_account"] else ""
    print(f"  {r['email']:30} → {status}{disp}{role}")

Clean a mailing list

import csv
from toolkitapi import Email

def clean_list(input_csv: str, output_csv: str) -> dict:
    """Remove undeliverable, disposable, and role addresses from a CSV list."""
    with open(input_csv, newline="") as f:
        rows = list(csv.DictReader(f))

    emails = [row["email"] for row in rows]
    kept, removed = [], []

    with Email(api_key="tk_...") as client:
        # Process in batches of 50
        results: list[dict] = []
        for i in range(0, len(emails), 50):
            resp = client.validate_batch(emails[i:i + 50])
            results.extend(resp["results"])

    for row, result in zip(rows, results):
        if result["deliverability"] in ("deliverable",) and \
           not result["is_disposable"] and \
           not result["is_role_account"]:
            kept.append(row)
        else:
            removed.append({**row, "reason": result["deliverability"]})

    fieldnames = list(rows[0].keys())
    with open(output_csv, "w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(kept)

    return {"kept": len(kept), "removed": len(removed)}

stats = clean_list("leads.csv", "clean_leads.csv")
print(stats)  # {"kept": 842, "removed": 158}

Segment results by deliverability

from toolkitapi import Email
from collections import defaultdict

addresses = [...]  # your list

buckets: dict[str, list[str]] = defaultdict(list)

with Email(api_key="tk_...") as client:
    for i in range(0, len(addresses), 50):
        resp = client.validate_batch(addresses[i:i + 50])
        for r in resp["results"]:
            buckets[r["deliverability"]].append(r["email"])

print("Deliverable:",   len(buckets["deliverable"]))
print("Undeliverable:", len(buckets["undeliverable"]))
print("Risky:",         len(buckets["risky"]))
print("Unknown:",       len(buckets["unknown"]))

Response Fields

validate and each record in validate-batch:

Field Type Description
email string Input email address
local_part string Part before @
domain string Domain part
syntax_valid bool Passes RFC 5322 syntax check
mx_found bool At least one MX record exists
mx_records list MX host names in preference order
smtp_code int | null SMTP response code (e.g. 250, 550)
smtp_message string | null Raw SMTP server response
is_deliverable bool | null SMTP verdict (null if inconclusive)
is_disposable bool Domain is a disposable/throwaway provider
is_free bool Domain is a free provider (gmail.com, etc.)
is_role_account bool Local part is a role/group address
is_catch_all bool | null Domain accepts all addresses (if probed)
deliverability string deliverable, undeliverable, risky, or unknown
confidence float 0.0–1.0 confidence in the deliverability verdict

validate-batch summary:

Field Type Description
total int Number of addresses submitted
results list Per-address result objects
summary.deliverable int Count of deliverable addresses
summary.undeliverable int Count of undeliverable addresses
summary.risky int Count of risky addresses
summary.unknown int Count of unknown-status addresses

Tip

The confidence field tells you how certain the API is about the deliverability verdict. A score above 0.9 is very reliable. Scores around 0.4–0.6 (typically risky results) indicate the SMTP probe was inconclusive — the server either greylisted the request or deferred the response. Consider treating these conservatively in critical sign-up flows.