All articles
Developer5 September 20268 min read

How to Block Fake Signups with Real-Time Email Verification

Every application that accepts an email address at signup is a target. Bots submit throwaway addresses to claim free trials. Users mistype their own email and never come back. Competitors probe your forms with fake accounts. The result: a growing list of addresses that were never real, never engaged, and will never convert.

Real-time email verification stops this at the moment of signup — before the address enters your database.

What counts as a "bad" signup

Not all invalid signups look the same. There are four distinct categories:

Mistyped addresses are the most common. gmial.com instead of gmail.com, a missing dot, a transposed character. The user is real and wants to sign up; they just made a typo. Catching this immediately lets you prompt for a correction — a better experience for the user, a valid address for you.

Disposable addresses are created specifically to claim one-time offers and never be reached again. Services like Mailinator, Temp-Mail, and thousands of lesser-known providers generate addresses that expire within hours or days. They pass format validation. They have valid MX records. But no real human checks them after signup.

Invalid addresses — the mailbox simply does not exist. The domain is real (gmail.com, outlook.com) but the specific address (notareal.address@gmail.com) has never been created. SMTP verification catches these; a format check does not.

Role-based addressesinfo@, admin@, support@ — are shared inboxes monitored by teams, not individuals. They're rarely engaged with personally, frequently bounce on commercial mail, and often belong to someone who isn't your actual buyer.

Why format validation isn't enough

Most developers start with a regex: does the string look like x@y.z? This catches obvious formatting errors but misses everything else. fakeperson1234@gmail.com is a perfectly formatted address that doesn't exist. user@disposable.biz passes regex and has MX records but expires tomorrow.

Format validation tells you the address could exist. Real-time verification tells you it does exist and is currently reachable.

How real-time verification works

The verification process happens in three stages:

1. Syntax and format check — basic structure validation. Fast and free. This runs first because there's no point doing DNS lookups on @@@.

2. DNS / MX lookup — checks whether the domain actually has mail servers configured. If example.xyz has no MX records, no mail can be delivered to it, regardless of what the address says.

3. SMTP handshake — connects to the domain's mail server and simulates sending a message to the specific address. The server responds with whether the mailbox exists. No email is actually sent; the connection is closed after the check. This is the definitive step — the one that catches invalid addresses on real domains.

Integrating real-time verification at signup

Verification should run server-side, after form submission, before account creation. Never run it client-side — your API key would be exposed and the check can be bypassed.

Next.js (App Router)

// app/api/auth/register/route.ts
import { NextRequest, NextResponse } from "next/server";

async function verifyEmail(email: string) {
  const res = await fetch(
    `https://api.stopbouncing.com/v1/verify?email=${encodeURIComponent(email)}`,
    {
      headers: { Authorization: `Bearer ${process.env.STOPBOUNCING_API_KEY}` },
      signal: AbortSignal.timeout(10_000),
    }
  );
  if (!res.ok) throw new Error("Verification service unavailable");
  return res.json() as Promise<{
    status: "valid" | "invalid" | "unknown";
    is_disposable: boolean;
    is_role_based: boolean;
  }>;
}

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

  // Format check first — fast, no API call
  if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
    return NextResponse.json({ error: "Enter a valid email address." }, { status: 400 });
  }

  // Real-time verification
  let verification;
  try {
    verification = await verifyEmail(email);
  } catch {
    // If verification is unavailable, fail open — don't block legitimate users
    verification = { status: "unknown", is_disposable: false, is_role_based: false };
  }

  if (verification.status === "invalid") {
    return NextResponse.json(
      { error: "That email address doesn't exist. Check for typos and try again." },
      { status: 422 }
    );
  }

  if (verification.is_disposable) {
    return NextResponse.json(
      { error: "Temporary email addresses are not accepted. Use your permanent address." },
      { status: 422 }
    );
  }

  // Proceed with account creation
  // await createUser({ email, password, name });
}

Express / Node.js

const express = require("express");
const app = express();
app.use(express.json());

async function verifyEmail(email) {
  const res = await fetch(
    `https://api.stopbouncing.com/v1/verify?email=${encodeURIComponent(email)}`,
    { headers: { Authorization: `Bearer ${process.env.STOPBOUNCING_API_KEY}` } }
  );
  return res.json();
}

app.post("/api/register", async (req, res) => {
  const { email } = req.body;

  let result;
  try {
    result = await verifyEmail(email);
  } catch {
    // Fail open if service is unavailable
    return continueRegistration(req, res);
  }

  if (result.status === "invalid") {
    return res.status(422).json({ error: "Email address does not exist." });
  }

  if (result.is_disposable) {
    return res.status(422).json({ error: "Disposable email addresses are not allowed." });
  }

  continueRegistration(req, res);
});

Django / Python

import os
import requests

def verify_email(email: str) -> dict:
    api_key = os.environ["STOPBOUNCING_API_KEY"]
    try:
        r = requests.get(
            "https://api.stopbouncing.com/v1/verify",
            params={"email": email},
            headers={"Authorization": f"Bearer {api_key}"},
            timeout=10,
        )
        r.raise_for_status()
        return r.json()
    except requests.RequestException:
        # Fail open — don't block signups if the service is down
        return {"status": "unknown", "is_disposable": False}


def register(request):
    email = request.POST.get("email", "").strip()
    result = verify_email(email)

    if result["status"] == "invalid":
        return error_response("That email address doesn't exist.")

    if result.get("is_disposable"):
        return error_response("Temporary email addresses are not accepted.")

    # Create account

What to do with "unknown" results

Some addresses can't be definitively confirmed — typically because the domain uses a catch-all configuration (every address at that domain appears to accept mail) or the mail server didn't respond in time.

The right policy depends on your application:

| Context | Recommendation | |---|---| | Free trial, high fraud risk | Reject or require email confirmation before granting access | | B2B SaaS, long sales cycle | Accept and flag — a real buyer at a catch-all domain is still valuable | | Email marketing list | Accept, then suppress on first hard bounce | | High-volume consumer app | Require double opt-in before counting toward engagement |

The worst choice is accepting unknown silently and treating the address as verified — that defeats the purpose of checking.

Always fail open

This is the most important implementation detail: if your verification API call fails (timeout, network error, service outage), let the signup proceed. The cost of blocking a real user is higher than the cost of occasionally accepting an unverified address.

try {
  verification = await verifyEmail(email);
} catch {
  // Service unavailable — continue rather than blocking the user
  verification = { status: "unknown", is_disposable: false };
}

Handle the two bad outcomes (invalid, disposable) firmly. Handle the unavailable service gracefully.

The compounding effect on deliverability

Blocking fake signups isn't just about list cleanliness in isolation. Invalid addresses that receive your welcome email generate hard bounces. Hard bounces raise your bounce rate. A bounce rate above 2% starts triggering spam filters at major providers. Once your sender reputation drops, even your legitimate emails land in spam — including for users who want to receive them.

Verification at signup prevents the problem from entering the system in the first place, rather than cleaning it up after the damage is done.

Getting started

The StopBouncing API takes one API call per signup. Accounts start with 100 free verifications — enough to verify the integration works before committing credits.

  1. Create an account — no credit card required
  2. Generate an API key from the API Keys section of your dashboard
  3. Add the server-side check to your signup handler using the examples above
  4. Deploy and monitor — your dashboard shows verification results per email

For full API documentation including response schemas, error codes, and rate limits, see the API reference.

Ready to clean your email list?

Verify thousands of addresses in minutes. No subscription — pay only for what you use.