How to Block Disposable Emails at Signup (Next.js Example)

If you offer a free trial, a free tier or referral credits, you already have disposable-email signups. Services like Mailinator, 10 Minute Mail and Guerrilla Mail hand out working inboxes in one click, so the same person can create account after account.

The cost is bigger than it looks:

  • Trial and free-tier abuse. One person, fifty accounts, fifty times your free quota.
  • Polluted metrics. Signups and activation look healthy while real users are flat.
  • Sender reputation. Welcome and onboarding emails to addresses that die within an hour turn into bounces, and bounces are exactly what mailbox providers use to judge you.

This guide shows how to block them on the server with a Next.js example, and how to handle the edge cases without annoying real users.

If your Next.js app already uses Better Auth, use the MailRambo Better Auth plugin guide instead of wiring the custom signup handler below. The plugin screens email/password signup using free fast mode and leaves your existing email-confirmation flow unchanged; it does not cover OAuth or magic-link signup.

Why a static domain list isn't enough

The obvious approach is an open-source list of disposable domains. It's a good start, but:

  • New disposable domains appear daily, and lists lag behind.
  • Some services rotate domains or let users bring their own.
  • A list can't tell a dead mailbox from a live one. jane.doe.19@gmail.com isn't disposable, but it might not exist either.

A verification API combines a maintained disposable list with a real mailbox check, so one call handles throwaways, typos and non-existent addresses. You can test the list part for free with the disposable email checker.

The server check

The check must run on the server; a browser check can be skipped with one request. With the MailRambo API:

// lib/verify-email.ts
type Verdict = { deliverable: boolean; reason: string };

export async function verifyEmail(email: string): Promise<Verdict | null> {
  try {
    const res = await fetch("https://www.mailrambo.com/v1/verify", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.MAILRAMBO_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ email }),
      cache: "no-store",
    });
    if (!res.ok) return null; // out of credits, rate limited, provider down
    return await res.json();
  } catch {
    return null;
  }
}

Returning null on any failure lets the caller fail open: if verification is unavailable, the signup still goes through.

Next.js server action

// app/signup/actions.ts
"use server";
import { verifyEmail } from "@/lib/verify-email";

const MESSAGES: Record<string, string> = {
  disposable: "Temporary email addresses aren't supported. Please use your regular email.",
  mailbox_not_found: "We couldn't find that mailbox. Check for typos?",
  catch_all: "We couldn't confirm that address. Please try another one.",
  invalid_syntax: "Please enter a valid email address.",
};

export async function signup(_: unknown, form: FormData) {
  const email = String(form.get("email") ?? "").trim();

  const verdict = await verifyEmail(email);
  if (verdict && !verdict.deliverable) {
    return { error: MESSAGES[verdict.reason] ?? "Please use a different email address." };
  }

  // await createUser(email, ...)
  return { ok: true };
}
// app/signup/page.tsx
"use client";
import { useActionState } from "react";
import { signup } from "./actions";

export default function SignupPage() {
  const [state, action, pending] = useActionState(signup, null);
  return (
    <form action={action}>
      <input name="email" type="email" required autoComplete="email" />
      {state?.error && <p role="alert">{state.error}</p>}
      <button disabled={pending}>{pending ? "Checking…" : "Create account"}</button>
    </form>
  );
}

Should you block catch-all addresses too?

MailRambo answers deliverable: false for catch-all domains, because the mailbox can't be confirmed. For signups, many teams choose to allow them, because plenty of real companies run catch-all mail servers:

const BLOCK = new Set(["disposable", "mailbox_not_found", "invalid_syntax", "spamtrap"]);
if (verdict && BLOCK.has(verdict.reason)) { /* reject */ }

For cold outreach lists, keep the strict default; catch-alls are where hidden bounces come from.

UX details that matter

  • Say why. "Temporary email addresses aren't supported" converts better than "invalid email".
  • Don't block role addresses (info@, admin@) at signup; small businesses use them. MailRambo returns them as deliverable: true with reason: role_account.
  • Check on submit, not on every keystroke. It's cheaper and less jumpy.
  • Use ?mode=fast if latency matters. It's free, answers in well under a second, and still catches disposable domains, typos and domains with no mail server. A full check takes 1-5 seconds.
  • Fail open when verification errors. Never lose a real user to a timeout.

Testing it

Use a free test key (mr_test_...) in development. It never contacts mail servers or uses credits:

Address Result
disposable@example.com deliverable: false, disposable
not_found@example.com deliverable: false, mailbox_not_found
deliverable@example.com deliverable: true, mailbox_exists
catch_all@example.com deliverable: false, catch_all

Summary

Blocking disposable emails takes one server-side API call. Use the reason code to show a clear message, decide explicitly how to treat catch-alls, and fail open on errors.

Check an address now with the disposable email checker, or get a free API key with 100 verifications a month.

Run these checks from your code

Verify addresses at signup, clean lists and read SPF/DKIM/DMARC with one API call. 100 free verifications a month, test keys that cost nothing, and the API on every plan.