Guide · Django 5.2 LTS / 6.0

In Django, the form says no
and the view never finds out.

A ValidationError raised in clean() rejects the registration through the machinery Django already has: form_invalid() re-renders with the error, the user row is never written, and CreateView stays three lines long. The same shape works in plain Django and in django-allauth.

01The form

# forms.py  (pip install requests — Django bundles no HTTP client)
import json

import requests
from django.conf import settings
from django.contrib.auth.forms import UserCreationForm
from django import forms


def verify_human(user_id: str, session_data: dict) -> dict | None:
    """Returns the useHUMA verdict, or None on outage (fail open)."""
    try:
        resp = requests.post(
            "https://humaverify.com/api/v1/verify",
            json={"userId": user_id, "sessionData": session_data},
            headers={"Authorization": f"Bearer {settings.HUMA_API_KEY}"},
            # (connect, read). Without this, requests waits FOREVER and a
            # useHUMA outage becomes a hung signup page.
            timeout=(3.05, 5),
        )
        resp.raise_for_status()
        return resp.json()
    except requests.RequestException:
        return None  # our outage must not become your outage


class RegistrationForm(UserCreationForm):
    # required=False on purpose: JS-disabled browsers and email-scanner
    # prefetches submit it empty, and absence is not evidence of a bot.
    huma_signals = forms.CharField(widget=forms.HiddenInput, required=False)

    def clean(self):
        cleaned_data = super().clean()
        try:
            signals = json.loads(cleaned_data.get("huma_signals") or "{}")
        except (TypeError, ValueError):
            signals = {}

        verdict = verify_human(
            user_id=cleaned_data.get("username", "anonymous"),
            session_data=signals,
        )
        # .get with a True default: an unexpected 200 body fails open
        # rather than raising KeyError in the middle of a signup.
        if verdict is not None and not verdict.get("human", True):
            raise forms.ValidationError(
                "We couldn't verify you're human. Please try again.",
                code="huma_not_human",
            )
        return cleaned_data

02The view, unchanged

# views.py — nothing special: rejection happens in the form
from django.urls import reverse_lazy
from django.views.generic.edit import CreateView

from .forms import RegistrationForm


class SignUpView(CreateView):
    form_class = RegistrationForm
    template_name = "registration/signup.html"
    success_url = reverse_lazy("login")

03The template carries the signals

<!-- templates/registration/signup.html -->
<script src="https://humaverify.com/huma.js"></script>
<form method="post">{% csrf_token %}{{ form.as_p }}
  <button>Sign up</button>
</form>
<script>
  document.querySelector("form").addEventListener("submit", function () {
    var f = document.querySelector("[name=huma_signals]");
    if (window.Huma && f) f.value = JSON.stringify(window.Huma.debug().features);
  });
</script>

04The allauth variant

# The django-allauth variant, which is what most real signup flows use.
# forms.py
from allauth.account.forms import SignupForm as AllauthSignupForm


class HumaSignupForm(AllauthSignupForm):
    huma_signals = forms.CharField(widget=forms.HiddenInput, required=False)

    def clean(self):
        # super().clean() FIRST: email and password uniqueness should fail
        # before you spend a verification call.
        cleaned_data = super().clean()
        try:
            signals = json.loads(cleaned_data.get("huma_signals") or "{}")
        except (TypeError, ValueError):
            signals = {}
        verdict = verify_human(cleaned_data.get("email", "anonymous"), signals)
        if verdict is not None and not verdict.get("human", True):
            raise forms.ValidationError("We couldn't verify you're human.")
        return cleaned_data


# settings.py
HUMA_API_KEY = os.environ["HUMA_API_KEY"]
ACCOUNT_FORMS = {"signup": "accounts.forms.HumaSignupForm"}

What bites

Why this instead of a CAPTCHA

A CAPTCHA charges every legitimate user for the existence of bots that increasingly solve puzzles anyway. This charges nobody: the person types, and the decision happens on your server from what already happened. It also answers a question a CAPTCHA does not, which is whether the session was an AI agent driving a real browser rather than a script. What it does not catch is written down at /limits, before you build on it.

Next.jsWordPressClerkAuth0Supabase AuthAuth.js / NextAuthFirebase Auth

Django for the app, useHUMA for humanity.

Free plan: 1,000 verifications a month · no card · no expiry · AI-agent verdict included.

Get your API key →

Form, view and allauth facts on this page were checked against Django's and django-allauth's live documentation on 26 Aug 2026, for Django 5.2 LTS, and are identical on 6.0. If something has drifted, tell us at team@humaverify.com and it gets fixed.

Help