Guides /

FastAPI

PY dependency

An async dependency that vets the address before your route runs. Bad addresses raise a 422.

The code

1 Keep the key on the server

MAILVET_KEY=mv_live_your_key

Set it wherever your server reads its environment. Never put it in client code.

2 Install httpx

pip install fastapi httpx

3 The dependency and the route

main.py

import logging
import os
from typing import Annotated

import httpx
from fastapi import Depends, FastAPI, HTTPException
from pydantic import BaseModel

log = logging.getLogger(__name__)
app = FastAPI()


class Signup(BaseModel):
    email: str


async def check_email(email: str) -> dict | None:
    # Returns MailVet's answer, or None if there is no answer.
    try:
        async with httpx.AsyncClient(timeout=5) as client:
            r = await client.get(
                "https://mailvet.dev/v1/check",
                params={"email": email},
                headers={"Authorization": f"Bearer {os.environ['MAILVET_KEY']}"},
            )
        r.raise_for_status()
        return r.json()
    except (httpx.HTTPError, KeyError, ValueError) as err:
        log.warning("mailvet: no answer: %s", err)
        return None


async def vetted_email(body: Signup) -> str:
    email = body.email.strip()
    check = await check_email(email)
    if check is None:
        return email  # a signup shouldn't die because of us

    if check["did_you_mean"]:
        local = email.rpartition("@")[0]
        raise HTTPException(422, f"Did you mean {local}@{check['did_you_mean']}?")
    if check["verdict"] == "invalid":
        raise HTTPException(422, "That address can't receive mail.")
    if check["verdict"] == "risky" and check["disposable"]:
        raise HTTPException(422, "Please use a permanent address, not a throwaway one.")
    return email


@app.post("/signup", status_code=201)
async def signup(email: Annotated[str, Depends(vetted_email)]):
    # Create the user here.
    return {"email": email}

What to do with the answer

did_you_mean, invalid and disposable raise HTTPException, so the route never runs and the client gets the message in detail. Role addresses and a risky from a DNS failure reach the route.

  • valid Let it through.
  • risky Read disposable. If it’s true, reject. If false, it’s a role address or DNS couldn’t be reached, so let it through.
  • invalid Bad format, or the domain has no mail server. Reject.

did_you_mean is empty or holds a domain, such as gmail.com for gamil.com. It doesn’t change the verdict. A known typo can come back valid because squatters give those domains a mail server, so check it first. See the docs for every field.

If MailVet is down, the signup goes through

httpx.HTTPError covers timeouts, network errors and the non-2xx answers from raise_for_status. ValueError covers bad JSON. All of them log and return None, so the dependency returns the address and the route runs.

A 401 or a 429 takes the same path, so a bad key or a used-up quota means addresses pass unchecked. Watch your logs for it.

Watch out

  • timeout=5 applies to each network step (connect, read and so on). It isn't a limit on the whole call.
  • httpx.AsyncClient per call is the short version. Under real load, share one client through FastAPI's lifespan.

Sources

Compared with these on November 5, 2026. If they’ve changed since, trust them over this page.

Other guides

Get API key