Mailbeam
Django + DRFBeginner15 minutesUpdated January 2025

Email Verification in Django

This tutorial shows how to add real-time email verification to a Django project. You'll build one reusable helper and wire it into both a classic Django form and a Django REST Framework (DRF) serializer, so invalid, disposable, and undeliverable addresses are rejected before a user is ever created.

What you'll build

  • A verify_email helper that wraps the Mailbeam SDK and fails open on API errors
  • A validate_email_deliverable validator for Django forms
  • A DRF serializer field validator for API-first projects

Prerequisites

  • Python 3.9+ and Django 4.2 or later
  • A Mailbeam API key (sign up free)
  • python-dotenv (or your preferred settings loader)

Step 1 — Install the SDK

pip install mailbeam

# Or add to requirements.txt:
echo "mailbeam>=1.0" >> requirements.txt
pip install -r requirements.txt

Step 2 — Configure settings

Keep the key out of source control. Load it from the environment in settings.py:

# settings.py
import os

MAILBEAM_KEY = os.environ["MAILBEAM_KEY"]

# Minimum quality score to accept (0–100). 60 is a sensible default.
MAILBEAM_MIN_SCORE = int(os.environ.get("MAILBEAM_MIN_SCORE", "60"))
# .env
MAILBEAM_KEY=mb_live_xxxxxxxxxxxxxxxxxxxx

Step 3 — Write a reusable verification helper

Put the integration in one place so forms, serializers, and views all share the same behavior.

# accounts/verification.py
import logging
import mailbeam
from django.conf import settings

logger = logging.getLogger(__name__)
_client = mailbeam.Client(api_key=settings.MAILBEAM_KEY)


def verify_email(email: str) -> tuple[bool, str | None]:
    """
    Returns (is_acceptable, reason_code).
    Fails OPEN (returns True) on Mailbeam API errors so an outage
    never blocks legitimate signups.
    """
    try:
        result = _client.verify_sync(email.strip().lower())
        if not result.valid or result.score < settings.MAILBEAM_MIN_SCORE:
            return False, result.reason or "invalid_email"
        return True, None
    except mailbeam.APIError as exc:
        logger.error("Mailbeam verification failed: %s", exc)
        return True, None  # fail open

Step 4 — Use it in a Django form

# accounts/forms.py
from django import forms
from django.core.exceptions import ValidationError
from .verification import verify_email


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

    def clean_email(self):
        email = self.cleaned_data["email"]
        ok, reason = verify_email(email)
        if not ok:
            raise ValidationError(
                "Please enter a valid, deliverable email address.",
                code=reason,
            )
        return email

Django runs clean_email automatically during form.is_valid(), so your view needs no changes:

# accounts/views.py
from django.shortcuts import render, redirect
from .forms import SignupForm


def signup(request):
    if request.method == "POST":
        form = SignupForm(request.POST)
        if form.is_valid():
            create_user(email=form.cleaned_data["email"])
            return redirect("welcome")
    else:
        form = SignupForm()
    return render(request, "accounts/signup.html", {"form": form})

Step 5 — Use it in a DRF serializer

For API-first projects, validate inside the serializer instead:

# accounts/serializers.py
from rest_framework import serializers
from .verification import verify_email


class SignupSerializer(serializers.Serializer):
    email = serializers.EmailField()
    password = serializers.CharField(write_only=True)

    def validate_email(self, value):
        ok, reason = verify_email(value)
        if not ok:
            raise serializers.ValidationError(
                f"Email rejected: {reason}"
            )
        return value
# accounts/views.py (DRF)
from rest_framework.generics import CreateAPIView
from .serializers import SignupSerializer


class SignupView(CreateAPIView):
    serializer_class = SignupSerializer
    # DRF returns 400 with field errors automatically if validation fails

Step 6 — Add caching

Avoid re-verifying the same address (for example, a user retrying a form). Django's cache framework works well:

# accounts/verification.py
from django.core.cache import cache

def verify_email(email: str) -> tuple[bool, str | None]:
    normalized = email.strip().lower()
    cached = cache.get(f"mb:{normalized}")
    if cached is not None:
        return cached

    try:
        result = _client.verify_sync(normalized)
        ok = result.valid and result.score >= settings.MAILBEAM_MIN_SCORE
        outcome = (ok, None if ok else (result.reason or "invalid_email"))
        cache.set(f"mb:{normalized}", outcome, timeout=86_400)  # 24h
        return outcome
    except mailbeam.APIError as exc:
        logger.error("Mailbeam verification failed: %s", exc)
        return True, None  # fail open (don't cache failures)

Testing

Mailbeam's test domains return deterministic results and don't count against your quota:

# accounts/tests.py
from django.test import TestCase
from .forms import SignupForm


class SignupFormTests(TestCase):
    def test_valid_email_passes(self):
        form = SignupForm(data={
            "email": "user@valid.mailbeam-test.dev",
            "password": "pass1234",
        })
        self.assertTrue(form.is_valid())

    def test_disposable_email_rejected(self):
        form = SignupForm(data={
            "email": "temp@disposable.mailbeam-test.dev",
            "password": "pass1234",
        })
        self.assertFalse(form.is_valid())
        self.assertIn("email", form.errors)

Best practices

PracticeWhy
Centralize in verification.pyForms, serializers, and views stay consistent
Fail open on mailbeam.APIErrorAn outage shouldn't break signups
Normalize (strip().lower())Consistent caching and fewer duplicate lookups
Cache for ~24hCuts API calls on retries and repeat submissions
Use test domainsDeterministic tests, no quota usage

Production checklist

  • MAILBEAM_KEY loaded from environment, not committed
  • Logging configured for mailbeam.APIError
  • Cache backend set (Redis or Memcached in production)
  • MAILBEAM_MIN_SCORE tuned for your funnel
  • User-facing error message is friendly in your templates

Next steps