Django REST Framework Email Verification Tutorial
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.