Integrations

How to block fake signups in FastAPI

To block fake signups in a FastAPI app, check the email, phone and IP of every new signup in your signup route, right before you create the user. With SignupVet that is one API call with requests, and about ten minutes of work.

Before you start

  1. Create a free account at signupvet.com/signup (250 checks a month, no card) and confirm your email.
  2. Create an API key in the dashboard.
  3. Add it to your server environment, never to browser code: SIGNUP_API_KEY=sv_live_...

1. Add a small helper

This helper calls the API with a 2 second timeout and fails open: if the API is slow, cannot be reached, the key is missing or your monthly checks are used up, it returns allow, so your signup route never breaks. It only sends a value as IP when it really is an IP address.

signup_check.py

"""Check a signup with SignupVet (pip install requests).

Fails open: if the API is slow, unreachable or returns an error, the signup is allowed.
"""
import ipaddress
import logging
import os

import requests

BASE_URL = os.environ.get("SIGNUPVET_URL", "https://www.signupvet.com")
FAIL_OPEN = {"recommendation": "allow", "risk_score": None, "reason_codes": [], "failed_open": True}


def _valid_ip(ip):
    """Only send real IP addresses (a test client or unix socket may give something else)."""
    try:
        return str(ipaddress.ip_address(ip.strip())) if ip else None
    except ValueError:
        return None


def check_signup(email=None, phone=None, ip=None, user_agent=None, timezone=None, timeout=2):
    body = {k: v for k, v in {"email": email, "phone": phone, "ip": _valid_ip(ip)}.items() if v}
    context = {k: v for k, v in {"user_agent": user_agent, "timezone": timezone}.items() if v}
    if context:
        body["context"] = context
    api_key = os.environ.get("SIGNUP_API_KEY")
    if not api_key:
        logging.warning("SIGNUP_API_KEY is not set, allowing signup")
        return dict(FAIL_OPEN)
    try:
        res = requests.post(
            f"{BASE_URL}/score",
            headers={"Authorization": f"Bearer {api_key}"},
            json=body,
            timeout=timeout,
        )
        res.raise_for_status()
        return res.json()
    except requests.RequestException as err:
        logging.warning("SignupVet check failed (%s), allowing signup", err)
        return dict(FAIL_OPEN)


if __name__ == "__main__":
    result = check_signup(email="someone@mailinator.com", ip="178.197.224.1")
    print(result["recommendation"], result["risk_score"], result["reason_codes"])

2. Call it in your signup route

app.py

"""FastAPI signup route with a SignupVet check.

pip install -r requirements.txt
SIGNUP_API_KEY=sv_live_... uvicorn app:app --reload
"""
from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel

from signup_check import check_signup

app = FastAPI()


class Signup(BaseModel):
    email: str
    password: str
    timezone: str | None = None


@app.post("/signup")
def signup(body: Signup, request: Request):
    # Behind a proxy, run uvicorn with --proxy-headers so request.client.host is the visitor's IP.
    check = check_signup(
        email=body.email,
        ip=request.client.host if request.client else None,
        user_agent=request.headers.get("user-agent"),
        timezone=body.timezone,
    )
    if check["recommendation"] == "block":
        raise HTTPException(400, "We could not create your account. Please use a different email address.")

    user_id = create_user(body.email, body.password)  # your existing signup
    if check["recommendation"] == "review":
        flag_for_review(user_id, check["reason_codes"])  # e.g. no free credits until verified
    return {"ok": True}


def create_user(email: str, password: str) -> str:
    return "user_123"


def flag_for_review(user_id: str, reasons: list[str]) -> None:
    print(f"review user {user_id}: {', '.join(reasons)}")
pip install requests fastapi uvicorn
SIGNUP_API_KEY=sv_live_... uvicorn app:app --reload

Three things matter here:

3. Send the visitor's IP, not your proxy's

Behind a load balancer or a platform like Railway, Render, Fly.io or Nginx, request.client.host is the proxy's address. Start uvicorn so it reads the visitor IP from the proxy headers:

uvicorn app:app --proxy-headers --forwarded-allow-ips="10.0.0.1"

Put your proxy's address in --forwarded-allow-ips. Use "*" only if your app cannot be reached except through that proxy, otherwise clients can fake their IP.

4. Decide what each result means for you

recommendationWhat to do
allowCreate the account as usual.
reviewCreate the account, but hold back free credits or the trial until the email is confirmed, or look at it manually.
blockDo not create the account. Show a neutral message.

For review, a common middle path is to create the account but unlock free credits only after the email is confirmed. The reason_codes tell you why, for example disposable_domain or signup_velocity_ip. All codes are listed in the documentation.

Good to know

Test it

Send a signup with test123@mailinator.com: the route answers 400 with your neutral message and no user is created. A normal address goes through with allow. We tested the example with FastAPI 0.162 and requests 2.34.

The full example is on GitHub: github.com/signupvet/examples. For the bigger picture, read how to stop fake signups.

Add it to your signup today

SignupVet checks the email, phone and IP of every new signup in one call and spots waves of signups from the same network. Free for 250 signups a month, no card needed.

Get 250 free checks   Read the docs