← Blog

How to block disposable email at signup

Check one flag at submit, choose your policy, and know exactly why a blocklist can't be your only line of defense.

Contents

You want to block disposable email at signup — the throwaway @mailinator.com, the ten-minute inbox a user grabs to claim your free tier and then abandons before your first onboarding email lands. This is a how-to. It comes down to reading one field at submit time, deciding what to do with it, and handling the part nobody puts on a pricing page: the list is never finished. If you want the background on why these domains exist, see what is a disposable email address.

Start with what you already have

Before you reach for an API, an off-the-shelf library already does the cheap 80 percent. Syntax validation catches typos; a DNS lookup tells you whether the domain can receive mail at all. In Python that is two lines:

from email_validator import validate_email, EmailNotValidError

try:
    validate_email("newuser@mailinator.com", check_deliverability=True)
except EmailNotValidError as e:
    print(e)  # raised on bad syntax or no deliverable MX

For a low-stakes contact form, that is often all you need, and you should not pay for more. But run the snippet above on newuser@mailinator.com and it passes: the syntax is fine and the MX records resolve. Deliverability is not the question you are actually asking. The question is whether this is a permanent address, and answering it requires a maintained list of disposable domains. That is the layer past the standard library.

The one field that matters: disposable

Send the address to the API and read the response:

curl -s https://api.boundstone.io/v1/verify/email \
  -H "Authorization: Bearer bs_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"newuser@mailinator.com"}'
{
  "valid_syntax": true,
  "domain": "mailinator.com",
  "mx_found": true,
  "disposable": true,
  "role_account": false,
  "free_provider": false,
  "checks": {
    "performed": ["syntax", "mx", "disposable_list", "role_list"],
    "not_performed": ["smtp_mailbox", "catch_all"]
  }
}

disposable is the boolean you branch on. Three neighbouring fields make the decision sharper: mx_found (the domain can actually receive mail), role_account (support@, admin@ — a shared inbox, not a person), and free_provider (a consumer provider like Gmail; a flag you can read, but note it is not a member of checks.performed).

The checks object is the point of the whole product. performed lists exactly what was tested; not_performed lists what was not. This response says plainly that no SMTP mailbox probe and no catch-all detection happened — because Boundstone does not do those today. That is why a disposable: true here is worth trusting: it is a narrow, list-backed claim, not a guess dressed up as certainty.

A signup handler that branches on policy

Whether a disposable address should be blocked, reviewed, or merely tagged is a business decision, not the API's. Keep that decision in one function so your policy is legible and easy to change:

function decide(check) {
  if (!check.mx_found)     return { action: "reject", reason: "undeliverable_domain" };
  if (check.disposable)    return { action: "reject", reason: "disposable_email" }; // hard block
  if (check.role_account)  return { action: "review", reason: "role_account" };     // flag
  return { action: "allow", reason: null };                                          // free_provider left for analytics
}

Then the Express handler stays thin:

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

  const r = await fetch("https://api.boundstone.io/v1/verify/email", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.BOUNDSTONE_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ email }),
  });
  const decision = decide(await r.json());

  if (decision.action === "reject") {
    return res.status(422).json({ error: "Please use a permanent email address." });
  }

  await createUser({ email, password, needsReview: decision.action === "review" });
  return res.status(201).json({ ok: true });
});

Three policies, one flag. Hard block returns 422 at submit — right for a free tier that abuse targets. Flag for review lets the signup through but marks it, so a human or a downstream rule decides — right when a false positive costs you a real customer. Tag for analytics stores the flag and does nothing else — right when you would rather measure disposable signups than fight them. The same decide function works identically whether you call it from Node, Python, or Go, because the response contract does not change across clients.

Blocklists are never exhaustive

Here is the honest limit. New disposable domains appear faster than any list absorbs them, so disposable: false is not proof of good faith — it means not on today's list. Do not build a wall out of one brick.

Pair the signals you already have in the same response. mx_found: false means the address is undeliverable no matter what, which is why the handler above rejects on it first. role_account: true means a shared inbox that may warrant a different flow. And treat what Boundstone does not do as the reason to trust what it does: smtp_mailbox and catch_all sit in not_performed because verifying that a specific mailbox exists is unreliable and we will not fake it. A passing result from Boundstone is deliberately narrow, and narrow is what makes it dependable. For the wider picture — velocity limits, IP signals, disposable filtering working together — see stopping signup fraud.

The short version

  • Read disposable from POST /v1/verify/email at submit. It is one boolean.
  • Pick a policy per field: block on disposable, reject on !mx_found, review on role_account, tag free_provider for analytics.
  • Keep the decision in one function so changing policy is a one-line edit, not a rewrite.
  • Never rely on the list alone — it trails reality by design; combine it with MX and role signals.
  • Trust the not_performed list. SMTP and catch-all are not done, so a passing result claims only what was actually checked.

Test any address free, no signup, with the email validator tool.

Frequently asked questions

How do you block disposable or temporary email addresses at signup?

Boundstone's email verify checks the address domain against a maintained list of known disposable providers and returns a disposable boolean, so you reject or flag the signup whenever it comes back true. That runs in the same single call as syntax, MX, and role checks, and the response lists exactly which checks ran (syntax, mx, disposable_list, role_list). Because the detection is list-based, it catches known throwaway domains reliably but can't guarantee a brand-new disposable domain nobody has catalogued yet, so it's best used alongside the other signals in the response.

If an email passes validation, does that mean it's a real, working inbox?

No, and Boundstone states this plainly on every response rather than letting you assume it. A valid result means the syntax is correct and the domain publishes MX records that can accept mail, but it does not prove a specific mailbox exists or that the domain isn't a catch-all, because SMTP mailbox verification and catch-all detection are listed under checks.not_performed. This honesty contract exists so you never mistake a deliverable-looking address for a confirmed-real one.

Should I also block role-based or free-provider emails, or just disposable ones?

Boundstone returns role_account (addresses like info@ or admin@) and free_provider (mailboxes on providers like gmail.com) as separate flags, so you can weigh them yourself instead of blocking blindly. Role and free addresses are legitimate for plenty of real signups, so treat them as risk signals rather than automatic rejections, and reserve hard blocks for disposable domains. Every response tells you precisely which checks were performed, so your signup rules are built on the facts each check actually establishes rather than on guesses.

Thomas Tsui

Founder of Boundstone — building phone, email and IP validation you can actually verify.

One honest API for email, phone and IP — every response lists what it checked and what it didn't claim to. Free tier: 250 credits/month, no card, credits never expire.

More from Boundstone — API documentation · Benchmark methodology · The benchmark series · Buyer's checklist