Integrations

How to block fake signups in Next.js

To block fake signups in a Next.js app, check the email, phone and IP of every new signup on the server, right before you create the user. With SignupVet that is one API call in a route handler or server action, and it takes about ten minutes.

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 server-only helper

This helper calls the API with a 2 second timeout and fails open: if the API cannot be reached or your monthly checks are used up, it returns allow, so your signup form never breaks. The server-only import makes sure the API key never ends up in browser code.

lib/signup-check.ts

// lib/signup-check.ts: server-only SignupVet check (fails open on errors and timeouts).
import "server-only";

export type SignupCheck = {
  recommendation: "allow" | "review" | "block";
  risk_score: number | null;
  reason_codes: string[];
  failed_open?: boolean;
};

const BASE_URL = process.env.SIGNUPVET_URL ?? "https://www.signupvet.com";
const FAIL_OPEN: SignupCheck = { recommendation: "allow", risk_score: null, reason_codes: [], failed_open: true };

export async function checkSignup(signup: {
  email?: string;
  phone?: string;
  ip?: string;
  userAgent?: string;
  timezone?: string;
}): Promise<SignupCheck> {
  try {
    const res = await fetch(`${BASE_URL}/score`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.SIGNUP_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        email: signup.email,
        phone: signup.phone,
        ip: signup.ip,
        context: { user_agent: signup.userAgent, timezone: signup.timezone },
      }),
      signal: AbortSignal.timeout(2000),
      cache: "no-store",
    });
    return res.ok ? ((await res.json()) as SignupCheck) : FAIL_OPEN;
  } catch {
    return FAIL_OPEN;
  }
}

/** Visitor IP on Vercel and most proxies: the first entry of x-forwarded-for. */
export function clientIp(headers: Headers): string | undefined {
  return headers.get("x-forwarded-for")?.split(",")[0]?.trim() || headers.get("x-real-ip") || undefined;
}

2. Check the signup before you create the user

In your signup route handler (App Router), call the helper first and only then create the account:

app/api/signup/route.ts

// app/api/signup/route.ts (Next.js App Router)
import { checkSignup, clientIp } from "@/lib/signup-check";

export async function POST(req: Request) {
  const { email, password, timezone } = await req.json();

  const check = await checkSignup({
    email,
    ip: clientIp(req.headers),
    userAgent: req.headers.get("user-agent") ?? undefined,
    timezone,
  });
  if (check.recommendation === "block") {
    return Response.json({ error: "We could not create your account. Please use a different email address." }, { status: 400 });
  }

  const user = await createUser({ email, password }); // your existing signup
  if (check.recommendation === "review") {
    await flagForReview(user.id, check.reason_codes); // e.g. no free credits until the email is confirmed
  }
  return Response.json({ ok: true });
}

// Replace these with your own user store.
async function createUser({ email }: { email: string; password: string }) {
  return { id: crypto.randomUUID(), email };
}
async function flagForReview(userId: string, reasons: string[]) {
  console.log(`review user ${userId}:`, reasons.join(", "));
}

Using a server action instead? Call checkSignup() the same way and read the visitor's IP with clientIp(await headers()), importing headers from next/headers.

3. 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.

Every answer also contains reason_codes, for example disposable_domain or signup_velocity_ip, so you can see why a signup was flagged. All codes are listed in the docs.

4. Test it

Sign up with an address like test123@mailinator.com: the result should be block because of a throwaway email provider. Your own normal address should get allow. You can also try addresses and IPs on the live demo first.

Good to know

The full examples are on GitHub: github.com/signupvet/examples.

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