Guide

Python

A Flask route using requests. The same call works in Django or FastAPI.

Browser

<script src="https://nolgard.com/v1/client.js" data-site-key="SITE_KEY" defer></script>
<script>
  document.querySelector('#signup').addEventListener('submit', async (event) => {
    event.preventDefault();
    await Nolgard.ready;
    const token = await Nolgard.execute({ action: 'signup' });
    const form = event.target;
    const response = await fetch('/signup', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token, email: form.elements.email.value })
    });
    // Show your own success or error message here.
  });
</script>

Flask

# app.py
import os

import requests
from flask import Flask, jsonify, request

app = Flask(__name__)


def nolgard_verify(token, action, account=None):
    payload = {
        "secret": os.environ["NOLGARD_SECRET"],
        "token": token,
        "remoteIp": request.remote_addr,
        "action": action,
    }
    if account:
        payload["account"] = account
    response = requests.post("https://nolgard.com/v1/verify", json=payload, timeout=3)
    if response.status_code >= 500:
        raise requests.HTTPError(response=response)
    return response.json()


@app.post("/signup")
def signup():
    data = request.get_json(silent=True) or {}
    email = data.get("email", "")
    try:
        result = nolgard_verify(data.get("token", ""), "signup", {"email": email})
    except (requests.RequestException, ValueError):
        result = {"success": True, "decision": "allow"}  # timeout or 5xx: fail open

    if not result.get("success"):
        if result.get("error") == "invalid-secret":
            return jsonify(error="try-again"), 503  # config error: fail closed
        if result.get("error") != "over-quota":
            return jsonify(error="rejected"), 403
        result = {"success": True, "decision": "allow"}  # over quota: fail open

    decision = result["decision"]
    if decision == "allow":
        create_account(email)
    elif decision == "limit":
        create_account(email, trial_days=3)
    elif decision == "shadow":
        pass  # same response, nothing created
    elif decision == "challenge":
        return jsonify(error="confirm-email"), 403
    else:
        return jsonify(error="rejected"), 403
    return jsonify(ok=True)

Supply your own create_account function. If you control the proxy in front of your app, wrap the app in Werkzeug's ProxyFix to set remote_addr to the visitor's address.

Django

# views.py
import json
import os

import requests
from django.http import JsonResponse
from django.views.decorators.http import require_POST


@require_POST
def signup(request):
    data = json.loads(request.body or b"{}")
    email = data.get("email", "")
    try:
        response = requests.post(
            "https://nolgard.com/v1/verify",
            json={
                "secret": os.environ["NOLGARD_SECRET"],
                "token": data.get("token", ""),
                "remoteIp": request.META.get("REMOTE_ADDR"),
                "action": "signup",
                "account": {"email": email},
            },
            timeout=3,
        )
        if response.status_code >= 500:
            raise requests.HTTPError(response=response)
        result = response.json()
    except (requests.RequestException, ValueError):
        result = {"success": True, "decision": "allow"}  # timeout or 5xx: fail open

    if not result.get("success"):
        if result.get("error") == "invalid-secret":
            return JsonResponse({"error": "try-again"}, status=503)  # config error: fail closed
        if result.get("error") != "over-quota":
            return JsonResponse({"error": "rejected"}, status=403)
        result = {"success": True, "decision": "allow"}  # over quota: fail open
    decision = result["decision"]
    if decision == "allow":
        create_account(email)
    elif decision == "limit":
        create_account(email, trial_days=3)
    elif decision == "shadow":
        pass
    elif decision == "challenge":
        return JsonResponse({"error": "confirm-email"}, status=403)
    else:
        return JsonResponse({"error": "rejected"}, status=403)
    return JsonResponse({"ok": True})

Keep Django's CSRF protection enabled and send its CSRF token with your form. Supply your own create_account function. Behind a proxy, use the address set by your trusted proxy. Do not trust unchecked forwarding headers.