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.