emailverifier.dev
Posts

Django REST Framework Email Verification Tutorial

| 4 min read | Usama Ejaz
Django REST email check cover with an email serializer document and a check mark.

This tutorial creates a Django REST Framework endpoint that verifies a signup email before a user or trial is written. You will scaffold the project, add the serializer and APIView, run the endpoint, test it with curl, and keep an upstream outage separate from an invalid address.

Create the Django project

mkdir django-email-check
cd django-email-check
python -m venv .venv

Activate the environment on macOS or Linux:

source .venv/bin/activate
pip install django djangorestframework
django-admin startproject config .
python manage.py startapp signup

On Windows PowerShell, activate it with .venv\Scripts\Activate.ps1. The tutorial uses:

django-email-check/
├─ config/
│  ├─ settings.py
│  └─ urls.py
└─ signup/
   ├─ serializers.py
   ├─ urls.py
   └─ views.py

Enable REST Framework

Add "rest_framework" and "signup" to INSTALLED_APPS in config/settings.py:

INSTALLED_APPS = [
    # Django defaults...
    "rest_framework",
    "signup",
]

Set the project key

Create an emailverifier.dev project and copy the API key. Export it before starting Django:

export EMAILVERIFIER_API_KEY=replace_with_your_project_key

On Windows PowerShell:

$env:EMAILVERIFIER_API_KEY =
  "replace_with_your_project_key"

The serializer reads the value from the server process. It is never accepted from the request body.

Build the serializer

Create signup/serializers.py:

import json
import os
import socket
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen

from rest_framework import serializers
from rest_framework.exceptions import APIException


class EmailVerificationUnavailable(APIException):
    status_code = 503
    default_detail = (
        "Email verification is temporarily unavailable."
    )


class SignupEmailSerializer(serializers.Serializer):
    email = serializers.EmailField(max_length=254)

    def validate_email(self, email: str) -> str:
        api_key = os.environ.get(
            "EMAILVERIFIER_API_KEY"
        )
        if not api_key:
            raise EmailVerificationUnavailable(
                "Email verification is not configured."
            )

        request = Request(
            "https://emailverifier.dev/api/v1/verify",
            data=json.dumps(
                {"email": email}
            ).encode("utf-8"),
            headers={
                "Content-Type": "application/json",
                "X-API-Key": api_key,
            },
            method="POST",
        )

        try:
            with urlopen(
                request,
                timeout=5,
            ) as response:
                result = json.load(response)
        except (
            HTTPError,
            URLError,
            TimeoutError,
            socket.timeout,
        ) as error:
            raise EmailVerificationUnavailable() from error

        if result.get("action") == "block":
            signals = result.get("signals", [])
            suggestion = result.get("suggestion")

            if "disposable_address" in signals:
                message = (
                    "Use a permanent email address."
                )
            elif suggestion:
                message = (
                    "Check the email address. "
                    f"Did you mean {suggestion}?"
                )
            else:
                message = (
                    "Check the email address "
                    "and try again."
                )

            raise serializers.ValidationError(message)

        self.context["verification"] = result
        return result["email"]

EmailField rejects empty or malformed input before validate_email spends a credit. The standard library performs the one external request. A network or authenticated API failure raises a real 503; a blocking email result remains a field-level 400.

Add the APIView

Create signup/views.py:

from rest_framework.response import Response
from rest_framework.views import APIView

from .serializers import SignupEmailSerializer


class SignupEmailView(APIView):
    def post(self, request):
        serializer = SignupEmailSerializer(
            data=request.data,
            context={},
        )
        serializer.is_valid(raise_exception=True)

        result = serializer.context["verification"]

        return Response({
            "accepted": True,
            "email": serializer.validated_data["email"],
            "needs_review": (
                result["action"] == "review"
            ),
            "result": result,
        })

Create signup/urls.py:

from django.urls import path

from .views import SignupEmailView

urlpatterns = [
    path(
        "signup/check-email/",
        SignupEmailView.as_view(),
        name="signup-email",
    ),
]

Replace config/urls.py with:

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("signup.urls")),
]

DRF's field-level validation documentation defines the validate_email hook used here.

Run and call the endpoint

python manage.py migrate
python manage.py runserver

In another terminal:

curl http://127.0.0.1:8000/signup/check-email/ \
  -H "Content-Type: application/json" \
  -d "{\"email\":\"person@example.com\"}"

An accepted response includes the normalized email, needs_review, and the full result. Missing or malformed input returns a DRF validation error before the external request.

Check the material failures

Run an empty request:

curl http://127.0.0.1:8000/signup/check-email/ \
  -H "Content-Type: application/json" \
  -d "{}"

Then stop Django, remove EMAILVERIFIER_API_KEY, restart it, and confirm a structurally valid email returns 503. That proves a configuration or network failure is not reported as a bad mailbox.

In permanent tests, patch signup.serializers.urlopen and return fixed allow, review, block, unknown, timeout, and HTTP-error fixtures. The application policy should be deterministic even when provider classifications or receiving servers change.

Use the result in registration

Call serializer.is_valid inside the existing registration view before creating the user. The full result supports four distinct paths:

  • deliverable: allow, with optional review for role-address or catch-all signals.
  • risky: block disposable addresses or surface a likely typo.
  • undeliverable: block a confirmed syntax, routing, or recipient failure.
  • unknown: continue through confirmation or restricted access because recipient evidence was inconclusive.

The production result-handling guide extends this serializer mapping to retries, logs, and queued decisions.

The REST API reference documents the exact payload. The tutorial is complete when valid JSON reaches the serializer, cheap failures avoid the external request, a block becomes a field error, and an unavailable check becomes 503.

Continue reading