Email verification checks whether an address can actually receive mail before you trust it — catching typos like gmial.com, disposable inboxes, and mailboxes that no longer exist. VerifyAnyEmail runs a live SMTP probe without ever sending a message and returns one status: deliverable, undeliverable, risky, or unknown. In Python you make a single HTTP call and branch on that value.
Why this runs server-side
The non-negotiable rule: never expose your API key in client-side code. A key baked into a browser script, a desktop app, or a public repo can be lifted by anyone and used to drain your credits. Python code running in your Django or FastAPI backend is server-side, so keep the key in an environment variable (os.environ) — never hard-coded, never returned to the client, never committed. The full checklist is in how to protect your API keys.
The request is always POST https://api.verifyany.email/v1/verify with an Authorization: Bearer vae_... header and a JSON body of { "email": "name@example.com" }. The response looks like { "ok": true, "result": { "status": "deliverable", "score": 98, ... } }. Full schema in the API docs.
Synchronous: requests + Django
The requests library is the simplest way in. This client never raises — every failure collapses to { "status": "unknown" }, so a slow or down API can't crash your signup flow.
# emails/verifier.py — synchronous client using requests. Server-side only.
import os
import requests
API_URL = "https://api.verifyany.email/v1/verify"
def verify_email(email: str) -> dict:
"""Returns the result dict. Never raises: every failure -> {"status": "unknown"}."""
try:
resp = requests.post(
API_URL,
headers={"Authorization": f"Bearer {os.environ['VAE_API_KEY']}"},
json={"email": email},
timeout=10, # seconds — never let a request hang
)
except requests.RequestException:
return {"status": "unknown"} # timeout / connection error -> fail open
if resp.status_code == 429 or resp.status_code >= 500:
return {"status": "unknown"}
data = resp.json()
if not data.get("ok"):
return {"status": "unknown"}
return data["result"] # {"status": ..., "subStatus": ..., "score": ..., ...}Drop that into a Django view. Verification runs after Django's own form validation, and only a confirmed undeliverable blocks the request.
# emails/views.py — verify at signup in a Django view.
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from .verifier import verify_email
@require_POST
def signup(request):
email = request.POST.get("email", "")
result = verify_email(email)
# Only block a DEFINITE negative. risky / unknown proceed (fail open).
if result.get("status") == "undeliverable":
return JsonResponse(
{"error": "That email address does not exist.",
"suggestion": result.get("suggestion")}, # "did you mean" fix
status=422,
)
is_risky = result.get("status") == "risky" # catch-all / role / disposable
# ... create the user, store is_risky for later review ...
return JsonResponse({"ok": True}, status=201)Async: httpx + FastAPI
If you're on an async stack, don't block the event loop with a synchronous call. Use httpx's async client. FastAPI's Pydantic EmailStr handles the cheap RFC syntax check for free, so the network probe only runs on well-formed input.
# main.py — async client with httpx, wired into FastAPI.
import os
import httpx
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr
app = FastAPI()
class SignupIn(BaseModel):
email: EmailStr # cheap RFC syntax check before we ever hit the network
async def verify_email(email: str) -> dict:
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.post(
"https://api.verifyany.email/v1/verify",
headers={"Authorization": f"Bearer {os.environ['VAE_API_KEY']}"},
json={"email": email},
)
except httpx.RequestError:
return {"status": "unknown"} # fail open on timeout / network error
if resp.status_code == 429 or resp.status_code >= 500:
return {"status": "unknown"}
body = resp.json()
return body["result"] if body.get("ok") else {"status": "unknown"}
@app.post("/signup")
async def signup(payload: SignupIn):
result = await verify_email(payload.email)
if result.get("status") == "undeliverable":
raise HTTPException(status_code=422, detail="Enter a valid email address.")
return {"ok": True, "risk": result.get("subStatus")}How to branch on the status
Four statuses, four actions — the policy matters more than the code:
- deliverable — real, reachable mailbox. Accept it.
- undeliverable — mailbox doesn't exist or the domain can't receive mail. Block it, and surface
result["suggestion"]if present (the "did you mean" typo fix). - risky — catch-all domain, role address (
info@,support@), or disposable inbox. Accept but storesubStatusto review or segment later. - unknown — greylisting, timeout, or a temporary SMTP failure. Fail open: let the signup through and re-verify later. Blocking on
unknownrejects real users over a transient hiccup.
This fail-open policy is what keeps verification from hurting conversion — details in verify emails at signup without hurting conversion.
Cleaning a list: a simple batch loop
To verify a CSV you already have, loop with retries and gentle pacing. This is fine for a few thousand rows.
# batch_clean.py — clean a CSV list, retrying on rate limits.
# For big lists, prefer the async /v1/verify/batch endpoint (see the batch guide).
import csv
import os
import time
import requests
API_URL = "https://api.verifyany.email/v1/verify"
HEADERS = {"Authorization": f"Bearer {os.environ['VAE_API_KEY']}"}
def verify_one(email: str) -> dict:
for attempt in range(3):
resp = requests.post(API_URL, headers=HEADERS,
json={"email": email}, timeout=15)
if resp.status_code == 429:
time.sleep(2 ** attempt) # exponential backoff: 1s, 2s, 4s
continue
resp.raise_for_status()
return resp.json()["result"]
return {"status": "unknown"}
with open("contacts.csv") as f, open("clean.csv", "w", newline="") as out:
writer = csv.writer(out)
writer.writerow(["email", "status", "score"])
for row in csv.reader(f):
email = row[0].strip()
r = verify_one(email)
writer.writerow([email, r["status"], r.get("score", "")])
time.sleep(0.1) # gentle pacing between requestsFor anything large, don't hammer the single-verify endpoint — submit the whole list to the asynchronous /v1/verify/batch endpoint and receive a signed webhook when it finishes. The patterns (idempotency, polling vs webhooks, pagination) are in processing large verification batches. If you consume the completion webhook, confirm your signature check with the Webhook Signature Tester.
Testing without spending credits
Every address at @test.verifyany.email returns a deterministic result and costs zero credits — no real mailbox is probed. Use them in pytest and local development:
deliverable@test.verifyany.email→status: deliverableundeliverable@test.verifyany.email→status: undeliverablerisky@test.verifyany.email→status: riskyunknown@test.verifyany.email→status: unknown
Point verify_email() at each address to cover every accept/flag/block branch without touching your balance. You can also try any address by hand in the Email Verifier.
Errors, timeouts and retries
- Always pass a timeout. Both clients above set one;
requestswill otherwise wait forever. A verification call must never hang a web request. - Fail open on 429 and 5xx. Return
unknownand proceed. In the batch loop, back off on429(the example uses exponential2 ** attemptseconds). - Don't retry 400 or 401. A
400is malformed input and401is a bad key — bugs to fix, not transient errors. (402means you're out of credits.) - Reuse a session/client. For loops, a single
requests.Session()or onehttpx.AsyncClientreuses connections and is noticeably faster than a fresh client per call.
Quick FAQ
Does this send an email to the person? No. The SMTP probe stops before delivery, so nothing reaches the recipient's inbox.
Sync or async? Use requests in Django and scripts; use httpx in FastAPI or any asyncio app so you don't block the event loop.
Should I verify inline or in a task queue? A single signup is fine inline with a 10-second timeout. For imports and list cleaning, use Celery/RQ or the batch API.
Working in another language? See the companion guides for PHP and Laravel and Node.js and Express, plus the full API documentation.
Sources
The examples use requests (sync) and httpx (async), wired into Django views and FastAPI.