ContactLintAPIMCPPricingFree toolsCompareGet a key

Home / Integrations / Django

Validate Emails in a Django Form clean_email Method

Published 2026-10-18

This guide adds a ContactLint check to a Django form through its clean_email method. Errors show up as normal field errors in your template, typos get a suggestion the person can accept or keep, and the form still validates if ContactLint does not answer within 3 seconds.

Where the check goes in Django

Browser checks are easy to skip, so the check that counts runs on the server, right before you save the address. In Django the natural place is the form's clean_email method. It runs during form.is_valid(), after Django's own EmailField syntax check passes. The same method works on a plain Form, a ModelForm, or a subclass of UserCreationForm that adds an email field.

Keep the secret key in the CONTACTLINT_KEY environment variable, never in templates or static files. Get a free key at /signup.

Setup: the API call, the form and the view

Install requests with pip install requests. Then add contactlint.py to your app:

import logging
import os

import requests

log = logging.getLogger(__name__)
URL = 'https://contactlint.com/v1/email/validate'


def lookup(email):
    """Return the ContactLint result dict, or None if ContactLint did not answer."""
    try:
        r = requests.post(
            URL,
            headers={'Authorization': 'Bearer ' + os.environ['CONTACTLINT_KEY']},
            json={'email': email},
            timeout=3,
        )
        if r.status_code != 200:
            log.warning('ContactLint returned %s, failing open', r.status_code)
            return None
        return r.json()['result']
    except (requests.RequestException, ValueError, KeyError) as exc:
        log.warning('ContactLint check skipped: %r', exc)
        return None

The form, in forms.py:

from django import forms

from .contactlint import lookup


class SignupForm(forms.Form):
    email = forms.EmailField()
    confirmed_email = forms.CharField(required=False, widget=forms.HiddenInput)

    def clean_email(self):
        email = self.cleaned_data['email']
        result = lookup(email)
        if result is None:
            return email  # fail open

        if result['verdict'] == 'invalid':
            raise forms.ValidationError('That email address cannot receive mail.')

        # Check did_you_mean before disposable: gmial.com is a typo and on the disposable list.
        suggestion = result.get('did_you_mean')
        if suggestion and self.data.get('confirmed_email') != email:
            self.data = self.data.copy()
            self.data['confirmed_email'] = email
            raise forms.ValidationError(
                f'Did you mean {suggestion}? Fix the address, or press Sign up again to keep it.'
            )

        if result.get('disposable'):
            raise forms.ValidationError('Please use a permanent email address.')
        return email

The view and template:

from django.shortcuts import redirect, render

from .forms import SignupForm


def signup(request):
    form = SignupForm(request.POST or None)
    if request.method == 'POST' and form.is_valid():
        # Create the user with form.cleaned_data['email'] here.
        return redirect('welcome')
    return render(request, 'signup.html', {'form': form})
<form method="post">
  {% csrf_token %}
  {{ form }}
  <button type="submit">Sign up</button>
</form>

Handling each verdict and did_you_mean

A kept typo still goes through the disposable check, so keeping jane@gmial.com ends with the permanent address error. Django escapes field errors in templates, so the suggestion is safe to display. For a clickable hint before submit, add the signup form widget with a cl_pub_ publishable key. The site builder guide shows the data-selector option for fields you cannot add attributes to.

Testing it

Open python manage.py shell with CONTACTLINT_KEY set, and replace yourapp with your app name:

>>> from yourapp.forms import SignupForm
>>> f = SignupForm({'email': 'jane@gmial.com'})
>>> f.is_valid(), f.errors['email']
>>> f = SignupForm({'email': 'jane@gmial.com', 'confirmed_email': 'jane@gmial.com'})
>>> f.is_valid(), f.errors['email']
>>> f = SignupForm({'email': 'x@mailinator.com'})
>>> f.is_valid(), f.errors['email']
  1. The first form fails with the "Did you mean jane@gmail.com?" error.
  2. The second fails with the permanent address error, because the typo domain is also disposable.
  3. The third fails with the permanent address error.
  4. Set a wrong key and repeat with any address. ContactLint returns 401, a warning is logged, and the form is valid.

Each check uses one of your 100 free checks per month.

Limits: no mailbox check, and fail open

Questions

How do I keep tests from calling the API?

Patch the lookup function where the form imports it, for example unittest.mock.patch('yourapp.forms.lookup', return_value=None) to test the fail open path. You can also return a dict such as {'verdict': 'risky', 'did_you_mean': 'jane@gmail.com', 'disposable': True} to test the suggestion branch.

Does this work with UserCreationForm or a ModelForm?

Yes. Subclass the form, add an email field if it does not have one (the default UserCreationForm does not), add the confirmed_email hidden field, and add the same clean_email method. Django calls clean_email during is_valid() on any form that has an email field.

Does ContactLint store the addresses my form checks?

No. Addresses are processed only to return a result and are not stored in the ContactLint database. ContactLint records which key made the check, the check type, whether it used a free check or a credit, and the time.

Other integrations

Get a free API key. 100 checks per month, no card required. Or try a live check first.