Checks the address in the form's clean_email, so a bad one shows up as an ordinary field error.
The code
1 Keep the key on the server
MAILVET_KEY=mv_live_your_keySet it wherever your server reads its environment. Never put it in client code.
2 Install requests
pip install requests3 Put the check in one place
accounts/mailvet.py
import logging
import os
import requests
log = logging.getLogger(__name__)
def check_email(email):
# Returns MailVet's answer, or None if there is no answer.
try:
r = requests.get(
"https://mailvet.dev/v1/check",
params={"email": email},
headers={"Authorization": f"Bearer {os.environ['MAILVET_KEY']}"},
timeout=5,
)
r.raise_for_status()
return r.json()
except (requests.RequestException, KeyError, ValueError) as err:
log.warning("mailvet: no answer: %s", err)
return None4 Use it in the form
accounts/forms.py
from django import forms
from django.core.exceptions import ValidationError
from .mailvet import check_email
class SignupForm(forms.Form):
email = forms.EmailField()
def clean_email(self):
email = self.cleaned_data["email"]
check = 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 ValidationError(
f"Did you mean {local}@{check['did_you_mean']}?",
code="typo",
)
if check["verdict"] == "invalid":
raise ValidationError("That address can't receive mail.", code="invalid")
if check["verdict"] == "risky" and check["disposable"]:
raise ValidationError(
"Please use a permanent address, not a throwaway one.",
code="disposable",
)
return emailWhat to do with the answer
did_you_mean, invalid and disposable raise ValidationError, which lands on the email field. Role addresses and a risky from a DNS failure return the address unchanged.
- 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
check_email catches network errors, timeouts, non-200 answers, a missing key and bad JSON, logs them and returns None. clean_email then returns the address and the form stays valid.
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
- Django skips
clean_emailwhen theEmailFielditself failed, so a malformed address doesn't spend a check. - Use this form for signup only. A form you also use for profile edits spends a check per save.
Sources
Compared with these on November 5, 2026. If they’ve changed since, trust them over this page.