---
title: "How to Handle Disposable Email Addresses at Signup"
description: "A disposable domain returns risky with reason disposable, never undeliverable. Flag it, gate value instead of entry, and reserve hard blocks for measured repeat abuse."
slug: "handle-disposable-email-addresses-at-signup"
date: 2026-10-02
updated: 2026-10-02
last_tested: 2026-10-02
summary: "Throwaway domains produce a real, working mailbox, so blocking them is a product rule rather than a validity fact: annotate the record, gate high-value actions, and only block abusers you can count."
cluster: "Signup flows"
intent: how-to
sources:
  - title: "nobounce.dev verdict taxonomy"
    url: "https://nobounce.dev/v1/config"
  - title: "nobounce.dev agent reference"
    url: "https://nobounce.dev/llms.txt"
  - title: "nobounce.dev OpenAPI specification"
    url: "https://nobounce.dev/openapi.json"
  - title: "MDN — Using the Fetch API"
    url: "https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch"
---

A disposable email address is not an invalid one. `user@mailinator.com` is a real mailbox on a real domain with working MX records; it just expires by design and its owner asked for it precisely to avoid giving you their permanent address. That distinction is why the validator returns `risky` with `reason: "disposable"` and never `undeliverable` — and why the decision of what to do about it belongs to your product, not to the DNS.

The short answer for a signup flow: do not block entry. Annotate the account, gate the things that cost you money until the address is proven durable, and hard-block only when you can count repeat abuse from the same actor. The rest of this page is the response contract and the wiring for those three policies.

## What the signal actually looks like

You can see the exact shape before integrating anything. `POST /demo/check` needs no key, but it is fixture-only: it resolves a frozen corpus (listed by `GET /v1/config`), so it cannot validate arbitrary real addresses. It includes three disposable providers — `mailinator.com`, `guerrillamail.com`, `10minutemail.com`:

```bash
curl -X POST https://nobounce.dev/demo/check \
  -H 'Content-Type: application/json' \
  -d '{"email":"user@mailinator.com"}'
```

```json
{"verdict":"risky","reason":"disposable","suggestion":null,
 "confidence":0.9,"checked":{"syntax":true,"mx":true,"typo":true,"disposable":true},
 "cached":false}
```

Three details matter. `suggestion` is `null` — unlike `risky` with `reason: "likely_typo"`, there is nothing to correct; the user typed exactly what they meant. `checked.disposable: true` tells you the disposable layer ran, the same way `checked.mx` reports on DNS. And the disposable check runs after syntax, typo and MX, so it only fires on an address that already resolved cleanly.

Live checks need a paid key — there is no free tier; hobby starts at $1/mo for 1,000 checks — obtained by redeeming an operator-minted access code or paying self-serve on the card or stablecoin rail. The key travels in the `Authorization` header, never in a URL:

```bash
curl -X POST https://nobounce.dev/v1/check \
  -H "Authorization: Bearer $NOBOUNCE_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"email":"user@mailinator.com"}'
```

## Why hard-blocking at entry is usually wrong

The tempting implementation is one line: `if (reason === "disposable") return 400`. It fails three ways.

First, it loses real customers. People use throwaway addresses to ration spam from services they do not yet trust, including yours. A signup that refuses the address refuses the person behind it; a percentage of them will not come back with a different one.

Second, the signal is per-domain, not per-person. The verdict tells you the domain is a throwaway provider. It tells you nothing about whether this signup is a bot farming free trials or a privacy-conscious human. Those are different problems, and the second one is solved by making the address worthless to farm — not by rejecting it.

Third, blocking one domain stops almost nothing. Disposable providers are a long tail that keeps growing, and a determined abuser can mint a fresh address faster than any list can age. Your own abuse signals — payment instruments, device or rate signals, repeated signups from one fingerprint — identify repeat actors far better than the domain of their mailbox.

## Three policies that compose

| policy | do | when |
|---|---|---|
| allow and annotate | accept the signup, store `reason: disposable` as account metadata | default for most products |
| gate value, not entry | accept the signup; require your own mailbox-confirmation click before trial credits, exports or paid features | freemium and trial products |
| block | reject with a clear message | only where you can count repeat abuse from the same actor |

The gate is the workhorse. A disposable address that survives your confirmation link was checked after creation, which most throwaway inboxes never are — and if it does confirm, the signup is as good as the address it used. Notice the gate is your mechanism, not the validator's: it is a normal transactional email you already send.

## Branch on reason, not verdict alone

`risky` covers more than disposable. `admin@hotmail.com` is `risky` with `reason: "role_account"` — a shared inbox, not a throwaway. Blocking role accounts at signup is a different product call (usually also "annotate") with a different message. If your handler switches on `verdict` only, you will eventually show a "temporary emails are not allowed" error to someone who typed a company inbox. The full reason enum is frozen and served by `/v1/config`, so you can branch on it safely.

A minimal server-side handler:

```js
const result = await checkEmail(input); // bounded call, null on failure

if (result === null) return { action: "accept", verdict: "unchecked" };

if (result.verdict === "risky" && result.reason === "disposable") {
  return { action: "accept", disposable: true, gateTrial: true };
}
if (result.verdict === "risky" && result.reason === "role_account") {
  return { action: "accept", roleAccount: true };
}
if (result.suggestion) {
  return { action: "suggest", suggestion: result.suggestion };
}
return { action: result.verdict === "undeliverable" ? "reject" : "accept" };
```

The `null` branch is not decoration: a validator timeout must never become a rejected signup. That rule, and the timeout that produces it, are covered in [Fail Open: Email Validation Must Never Block a Signup](https://nobounce.dev/blog/fail-open-email-validation-signup/). On the browser side, `risky` is a state your form has to render distinctly from a hard error — [Handle Email Validation Verdicts in a React Signup Form](https://nobounce.dev/blog/email-validation-verdicts-in-react-form/) maps all four. And the contrast with `likely_typo` matters: when `risky` arrives *with* a suggestion, offer the correction instead of a warning, as [Suggest the Correction Instead of Rejecting the Signup](https://nobounce.dev/blog/suggest-the-correction-not-rejection/) argues.

One logging note: a verdict response contains no address, and batch results are keyed by `email_sha256`, so log the verdict fields and the reason — never the submitted address, and never the suggestion, which contains one.

The disposable verdict is a fact about a domain. What your product does with it is the actual decision, and the signup itself should stay open either way. For the full integration procedure — key handling, all four verdicts, the verification step — give your coding agent [/integrate.md](https://nobounce.dev/integrate.md).
