Email Verification in Python
This tutorial walks through integrating Mailbeam email verification into a Python web application. You'll see examples for both FastAPI (async, recommended for new projects) and Flask (sync, for existing apps).
What you'll build
- A reusable
validate_emailFastAPI dependency - A sync
verify_emailhelper for Flask or other sync frameworks - Proper error handling that doesn't block users on API failures
Prerequisites
- Python 3.9 or later
- A virtual environment (venv, poetry, or conda)
- A Mailbeam API key (sign up free)
Step 1 — Add a small client
There is no SDK to install. Mailbeam is a single HTTP endpoint, so a short module in your own codebase covers it, with nothing to keep upgraded. Official SDKs are on the roadmap; this is what to do today.
pip install httpx # or use requests, if that is already in the project# app/mailbeam.py
import os
import httpx
ENDPOINT = "https://api.mailbeam.dev/v1/verify"
HEADERS = {"Authorization": f"Bearer {os.environ['MAILBEAM_KEY']}"}
class MailbeamError(Exception):
"""Anything that stopped us getting a verdict."""
async def verify_email(email: str, timeout: float = 3.0) -> dict:
try:
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.post(ENDPOINT, json={"email": email}, headers=HEADERS)
response.raise_for_status()
return response.json()
except httpx.HTTPError as exc:
raise MailbeamError(str(exc)) from exc
def verify_email_sync(email: str, timeout: float = 3.0) -> dict:
try:
response = httpx.post(ENDPOINT, json={"email": email}, headers=HEADERS, timeout=timeout)
response.raise_for_status()
return response.json()
except httpx.HTTPError as exc:
raise MailbeamError(str(exc)) from excStep 2 — Set your environment variable
# .env
MAILBEAM_KEY=mb_live_xxxxxxxxxxxxxxxxxxxxLoad it with python-dotenv or your framework's preferred approach:
import os
from dotenv import load_dotenv
load_dotenv()
MAILBEAM_KEY = os.environ["MAILBEAM_KEY"]Step 3 — FastAPI: create the validation dependency
# app/dependencies.py
import logging
from fastapi import HTTPException
from app.mailbeam import MailbeamError, verify_email
async def validate_email(email: str) -> str:
"""
FastAPI dependency that verifies an email address.
Raises HTTP 422 if the email is invalid or low-quality.
Fails open (passes through) on Mailbeam API errors.
"""
try:
result = await verify_email(email)
except MailbeamError as exc:
# Log but don't block — our outage is not the user's problem.
logging.error("Mailbeam verification failed: %s", exc)
return email
if not result["valid"] or result["score"] < 60:
raise HTTPException(
status_code=422,
detail={
"error": "Please provide a valid email address.",
"code": result["reason"] or "invalid_email",
},
)
return emailStep 4 — Add to your signup endpoint
# app/routes/auth.py
from fastapi import APIRouter, Depends
from pydantic import BaseModel, EmailStr
from typing import Annotated
from app.dependencies import validate_email
router = APIRouter()
class SignupRequest(BaseModel):
email: EmailStr
password: str
@router.post("/signup", status_code=201)
async def signup(
request: SignupRequest,
# The dependency runs BEFORE the handler and raises 422 if invalid
verified_email: Annotated[str, Depends(validate_email)],
):
user = await create_user(email=verified_email, password=request.password)
return {"user": user}Your main app:
# app/main.py
from fastapi import FastAPI
from app.routes.auth import router
app = FastAPI()
app.include_router(router, prefix="/api/auth")Step 5 — Flask (sync) version
# app/utils/email.py
import logging
from app.mailbeam import MailbeamError, verify_email_sync
def check_email(email: str) -> tuple[bool, str | None]:
"""
Returns (is_valid, reason_code).
Fails open (returns True) on API errors.
"""
try:
result = verify_email_sync(email)
except MailbeamError as exc:
logging.error("Mailbeam verification failed: %s", exc)
return True, None # fail open
if not result["valid"] or result["score"] < 60:
return False, result["reason"] or "invalid_email"
return True, None
# app/routes/auth.py (Flask)
from flask import Blueprint, request, jsonify
from app.utils.email import check_email
bp = Blueprint("auth", __name__)
@bp.post("/api/auth/signup")
def signup():
data = request.get_json()
email = data.get("email", "")
is_valid, reason = verify_email_sync(email)
if not is_valid:
return jsonify({"error": "Please provide a valid email.", "code": reason}), 422
user = create_user(email=email, password=data.get("password"))
return jsonify({"user": user}), 201Testing the integration
# tests/test_signup.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_signup_with_valid_email():
# Uses Mailbeam's test domain — always returns valid, doesn't count against quota
response = client.post(
"/api/auth/signup",
json={"email": "user@valid.mailbeam-test.dev", "password": "pass1234"},
)
assert response.status_code == 201
def test_signup_with_invalid_email():
response = client.post(
"/api/auth/signup",
json={"email": "user@invalid.mailbeam-test.dev", "password": "pass1234"},
)
assert response.status_code == 422
assert response.json()["detail"]["code"] is not None
def test_signup_with_disposable_email():
response = client.post(
"/api/auth/signup",
json={"email": "temp@disposable.mailbeam-test.dev", "password": "pass1234"},
)
assert response.status_code == 422Adding a cache layer
# app/dependencies.py
from functools import lru_cache
import asyncio
# Simple in-process TTL cache using a dict (use Redis in production)
_cache: dict[str, tuple] = {}
async def validate_email(email: str) -> str:
normalized = email.lower().strip()
if normalized in _cache:
valid, score, reason = _cache[normalized]
if not valid or score < 60:
raise HTTPException(
status_code=422,
detail={"error": "Please provide a valid email.", "code": reason},
)
return email
try:
result = await verify_email(normalized)
except MailbeamError:
return email # fail open
_cache[normalized] = (result["valid"], result["score"], result["reason"])
if not result["valid"] or result["score"] < 60:
raise HTTPException(
status_code=422,
detail={"error": "Please provide a valid email.", "code": result["reason"]},
)
return emailBest practices
| Practice | Why |
|---|---|
| Use the async client with FastAPI | Avoid blocking the event loop |
| Use the sync client with Flask | No async overhead in sync frameworks |
Fail open on MailbeamError | API outages shouldn't break signup |
| Normalize email before caching | Prevent cache misses from case differences |
| Use test domains in pytest | Isolated tests, no quota usage |
Production checklist
-
MAILBEAM_KEYin environment secrets, not in code - Error logging configured (
logging.error, notprint) - Cache configured (Redis for multi-worker deployments)
- Test domains used in
pytestfixtures - 422 error message is user-friendly on your frontend