# How to block disposable email at signup

_2026-07-18 · Boundstone (https://boundstone.io/blog/block-disposable-email-signup)_


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](/blog/what-is-disposable-email).

## 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:

```python
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:

```bash
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"}'
```

```json
{
  "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:

```javascript
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:

```javascript
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](/use-cases/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](/tools/email-validator).
