Private betaMailbeam is in private beta — the public API isn't live yet.Join the waitlist

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 — Add an HTTP client

There is no SDK to install. Mailbeam is one HTTP endpoint, so the integration below is all the client you need. Official SDKs are on the roadmap.

pip install httpx        # or use requests, if that is already in the project

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 httpx
from django.conf import settings

logger = logging.getLogger(__name__)

ENDPOINT = "https://api.mailbeam.dev/v1/verify"


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:
        response = httpx.post(
            ENDPOINT,
            json={"email": email.strip().lower()},
            headers={"Authorization": f"Bearer {settings.MAILBEAM_KEY}"},
            timeout=3.0,
        )
        response.raise_for_status()
        result = response.json()
    except httpx.HTTPError as exc:
        logger.error("Mailbeam verification failed: %s", exc)
        return True, None  # fail open

    if not result["valid"] or result["score"] < settings.MAILBEAM_MIN_SCORE:
        return False, result["reason"] or "invalid_email"
    return True, None

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