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:
curl -X POST https://nobounce.dev/demo/check \
-H 'Content-Type: application/json' \
-d '{"email":"user@mailinator.com"}'
{"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:
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:
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. 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 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 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.