Email Verification in Python: Django and FastAPI Examples

Python's standard library won't tell you whether an email address can receive mail. email.utils.parseaddr happily accepts x@y, and even strict validators only check the format. To know whether a mailbox exists, you have to ask the mail server. The simplest way to do that is an API.

This guide shows how to verify addresses from plain Python, a Django form, and a FastAPI endpoint, with sensible error handling and tests that don't spend money.

You'll need a MailRambo API key. The free plan includes 100 verifications a month, and free test keys (mr_test_...) return canned answers.

The client: ten lines with requests

# verify.py
import os
import requests

API = "https://www.mailrambo.com/v1/verify"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['MAILRAMBO_KEY']}"


class VerificationError(Exception):
    def __init__(self, status, code, message):
        super().__init__(message)
        self.status, self.code = status, code


def verify_email(email: str) -> dict:
    r = session.post(API, json={"email": email}, timeout=30)
    body = r.json()
    if not r.ok:
        raise VerificationError(r.status_code, body.get("error"), body.get("message"))
    return body  # {"email", "deliverable", "reason", "credits_remaining"}
>>> verify_email("jane@acme.com")
{'email': 'jane@acme.com', 'deliverable': True, 'reason': 'mailbox_exists', 'credits_remaining': 49}

deliverable is True only when the mailbox is confirmed. Catch-all domains, disposable providers and mailboxes that don't answer clearly all come back as False, with a reason such as catch_all, disposable, mailbox_not_found or unverifiable. Syntactically invalid input returns invalid_syntax and doesn't use a credit.

Async version with httpx

For async frameworks, use httpx.AsyncClient and reuse it across requests:

import os
import httpx

client = httpx.AsyncClient(
    base_url="https://www.mailrambo.com/v1",
    headers={"Authorization": f"Bearer {os.environ['MAILRAMBO_KEY']}"},
    timeout=30,
)


async def verify_email(email: str) -> dict:
    r = await client.post("/verify", json={"email": email})
    body = r.json()
    if r.is_error:
        raise VerificationError(r.status_code, body.get("error"), body.get("message"))
    return body

Django: validate in the form

Put verification in the form's clean_email so the error appears next to the field:

# accounts/forms.py
import logging
from django import forms
from .verify import verify_email, VerificationError

log = logging.getLogger(__name__)

MESSAGES = {
    "mailbox_not_found": "We couldn't find that mailbox. Check for typos?",
    "disposable": "Please use a permanent email address.",
    "catch_all": "We couldn't confirm this address. Try another one?",
}


class SignupForm(forms.Form):
    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)

    def clean_email(self):
        email = self.cleaned_data["email"]
        try:
            result = verify_email(email)
        except (VerificationError, Exception) as e:
            log.warning("email verification skipped: %s", e)  # fail open
            return email
        if not result["deliverable"]:
            raise forms.ValidationError(MESSAGES.get(result["reason"], "Please use a different email address."))
        return email

forms.EmailField does the cheap syntax check first, so obviously broken input never reaches the API.

Tip: add ?mode=fast for a free check that answers in under a second (syntax, typos like gmial.com, domains with no mail server, disposable providers). Use it inline on the form and keep the full check for when you need a confirmed yes/no. See fast mode.

FastAPI: validate in the endpoint

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr

app = FastAPI()


class Signup(BaseModel):
    email: EmailStr
    password: str


@app.post("/signup", status_code=201)
async def signup(data: Signup):
    try:
        result = await verify_email(data.email)
    except Exception:
        result = {"deliverable": True}  # fail open if verification is unavailable
    if not result["deliverable"]:
        raise HTTPException(400, detail={"field": "email", "reason": result["reason"]})
    # ... create the user
    return {"ok": True}

Error handling that won't hurt signups

Errors are always JSON, {"error": "<code>", "message": "..."}:

Status error Recommended handling
401 invalid_api_key Configuration bug; alert
402 insufficient_credits Fail open, alert, top up
429 rate_limited Back off and retry (600 requests/min per key; full checks are fair-use at ~5/s)
503 provider_unavailable Retry once; the credit was refunded

The rule: never block a real user because verification failed. Let them through and log it.

Testing without credits

Create a test key on the API Keys page and set it in your test settings. Test keys return answers based on the part before the @:

# tests/test_signup.py  (MAILRAMBO_KEY=mr_test_... in the test environment)
def test_rejects_disposable(client):
    r = client.post("/signup", json={"email": "disposable@example.com", "password": "x" * 12})
    assert r.status_code == 400

def test_accepts_real_mailbox(client):
    r = client.post("/signup", json={"email": "deliverable@example.com", "password": "x" * 12})
    assert r.status_code == 201

Other magic addresses: catch_all@, not_found@, role@, spamtrap@, unverifiable@. See test mode in the docs.

Cleaning an existing list

For a CSV or a users table, batch up to 200 addresses per call and poll:

import time, uuid

def verify_many(emails):
    start = session.post(
        "https://www.mailrambo.com/v1/verify/batch",
        json={"emails": emails},
        headers={"Idempotency-Key": str(uuid.uuid4())},  # retry-safe
        timeout=30,
    ).json()
    while True:
        batch = session.get(f"https://www.mailrambo.com/v1/verify/batch/{start['batch_id']}", timeout=30).json()
        if batch["status"] == "completed":
            return {row["email"]: row for row in batch["results"]}
        time.sleep(3)

Summary

  • Syntax validation (EmailField, EmailStr) filters garbage; an API call confirms the mailbox.
  • Branch on deliverable, and use reason for a helpful message.
  • Fail open on errors, test with mr_test_ keys, and use Idempotency-Key for batches.

Try an address in the free email verifier, or get a free API key.

Run these checks from your code

Verify addresses at signup, clean lists and read SPF/DKIM/DMARC with one API call. 100 free verifications a month, test keys that cost nothing, and the API on every plan.