Integrations

How to block fake signups in Express

To block fake signups in an Express 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, no new dependencies, 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 or your monthly checks are used up, it returns allow, so your signup route never breaks. It uses the built-in fetch of Node 18 and later.

signup-check.js

// signup-check.js: check a signup with SignupVet (Node 18+, no dependencies).
//
// Fails open: if the API is slow, unreachable or returns an error (for example
// because your monthly checks are used up), the signup is allowed. Your signup
// form never breaks because of this check.

const FAIL_OPEN = Object.freeze({ recommendation: "allow", risk_score: null, reason_codes: [], failed_open: true });

/**
 * @param {{ email?: string, phone?: string, ip?: string, countryHint?: string,
 *           userAgent?: string, timezone?: string, acceptLanguage?: string }} signup
 * @returns {Promise<{ recommendation: "allow" | "review" | "block", risk_score: number | null,
 *                     reason_codes: string[], failed_open?: boolean }>}
 */
export async function checkSignup(signup, { timeoutMs = 2000 } = {}) {
  const body = {
    email: signup.email,
    phone: signup.phone,
    ip: signup.ip,
    country_hint: signup.countryHint,
    context: {
      user_agent: signup.userAgent,
      timezone: signup.timezone,
      accept_language: signup.acceptLanguage,
    },
  };
  const baseUrl = process.env.SIGNUPVET_URL ?? "https://www.signupvet.com";
  try {
    const res = await fetch(`${baseUrl}/score`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.SIGNUP_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
      signal: AbortSignal.timeout(timeoutMs),
    });
    if (!res.ok) {
      console.warn(`SignupVet returned ${res.status}, allowing signup`);
      return FAIL_OPEN;
    }
    return await res.json();
  } catch (err) {
    console.warn(`SignupVet unreachable (${err?.name ?? "error"}), allowing signup`);
    return FAIL_OPEN;
  }
}

2. Call it in your signup route

server.js

// Express signup route with a SignupVet check before the account is created.
// Run: npm install && SIGNUP_API_KEY=sv_live_... npm start
import express from "express";
import { checkSignup } from "../node/signup-check.js";

const app = express();
app.use(express.json());
// Behind a proxy or load balancer (Heroku, Render, Railway, Nginx ...), so req.ip is the visitor's IP.
app.set("trust proxy", 1);

app.post("/signup", async (req, res) => {
  const { email, password, phone, timezone } = req.body ?? {};

  const check = await checkSignup({
    email,
    phone,
    ip: req.ip,
    userAgent: req.get("user-agent"),
    acceptLanguage: req.get("accept-language"),
    timezone, // optional: Intl.DateTimeFormat().resolvedOptions().timeZone from the browser
  });

  if (check.recommendation === "block") {
    // Keep the message neutral; do not tell bots which signal tripped.
    return res.status(400).json({ error: "We could not create your account. Please use a different email address." });
  }

  const user = await createUser({ email, password });
  if (check.recommendation === "review") {
    // For example: hold free credits until the email is confirmed, or queue for a manual look.
    await flagForReview(user.id, check.reason_codes);
  }
  res.json({ ok: true });
});

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

app.listen(3000, () => console.log("Listening on http://localhost:3000"));

Three things matter here:

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.

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.

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