Check any domain or address against 76,826 known disposable email providers — instantly, and for free.
No rate limit (unless abused)
An inbox handed out instantly, with no signup, and thrown away minutes later. Services like mailinator.com or yopmail.com exist so someone can clear a "confirm your email" step without ever giving a real address.
Accounts abandoned the moment they are created, quietly inflating every growth number you report.
One person, a fresh address each time, taking the same free trial for as long as it works.
Onboarding sequences and receipts sent to an inbox that stopped existing an hour ago.
A full address works too — everything before the @ is discarded before the lookup. Subdomains resolve through their parent, so inbox.mailinator.com is caught by mailinator.com.
false and 400 are different claims — checked and clean versus that was not a domain. A typo should never read as a clean address.
/c/<domain>| 200 | {"disposable":false} | clean |
| 200 | {"disposable":true} | disposable |
| 400 | {"error":"…"} | not a domain |
curl https://disposable.aops.network/c/mailinator.com {"disposable":true} curl https://disposable.aops.network/c/gmail.com {"disposable":false}
// Browser, Node 18+, Deno, Bun — no dependencies async function isDisposable(email) { const domain = email.split('@').pop(); const res = await fetch( `https://disposable.aops.network/c/${encodeURIComponent(domain)}` ); if (!res.ok) return false; // 400 = not a domain const { disposable } = await res.json(); return disposable; }
function isDisposable(string $email): bool {
$domain = substr(strrchr($email, '@'), 1) ?: $email;
$json = @file_get_contents(
'https://disposable.aops.network/c/' . rawurlencode($domain)
);
if ($json === false) return false; // unreachable or 400
return json_decode($json, true)['disposable'] ?? false;
}import requests
def is_disposable(email: str) -> bool:
domain = email.rsplit("@", 1)[-1]
r = requests.get(f"https://disposable.aops.network/c/{domain}", timeout=2)
if r.status_code != 200: # 400 = not a domain
return False
return r.json()["disposable"]func isDisposable(email string) bool {
domain := email[strings.LastIndex(email, "@")+1:]
resp, err := http.Get("https://disposable.aops.network/c/" + domain)
if err != nil || resp.StatusCode != 200 {
return false // 400 = not a domain
}
defer resp.Body.Close()
var out struct {
Disposable bool `json:"disposable"`
}
json.NewDecoder(resp.Body).Decode(&out)
return out.Disposable
}require 'net/http' require 'json' def disposable?(email) domain = email.split('@').last res = Net::HTTP.get_response( URI("https://disposable.aops.network/c/#{domain}") ) return false unless res.code == '200' JSON.parse(res.body)['disposable'] end
Each example sends only the domain, so the local part never leaves your process. Treat a non-200 as "unknown" rather than failing the signup — a check that is down should not block a real user.
The answer is 19 bytes of JSON, so a web framework would be the entire workload — parsing headers into a hash map costs more than the question does. So there isn't one. HTTP is parsed inline, both answers are byte-for-byte constants compiled into the binary, and serving a request allocates nothing at all.
No garbage collector means no pauses, so the tail latency you see is the network rather than the server. It builds to one statically linked binary carrying its own libc — which is why the Docker image needs no operating system inside it.
| /server | 1,946,496 |
| /database | 1,176,939 |
| Docker image | 3.0 MB |
Two files, no distribution, no package manager, no shell — nothing underneath collecting CVEs between releases.
One core of a mid-2010s Haswell, capped at 32 MB of RAM, behind a proxy that terminates TLS on the same two vCPUs. The container speaks plain HTTP on a port that is not even published.
| cpu | Haswell @ 2.594 GHz |
| cores | 2 vCPU, no SMT |
| memory | 3.7 GB, no swap |
| disk | 38 GB |
| hypervisor | KVM |
| cpu | 1 of the 2 cores |
| memory | 32 MB |
| workers | 1, from the cgroup quota |
| memory used, idle | 5.9 MB |
| peak seen | 7.7 MB |
The 5.9 MB is mostly the blocklist, plus the page held in three encodings. It grows under load — every open connection takes an 8 KB read buffer, so a few hundred at once add a couple of MB — and the highest ever seen is 7.7, less than a quarter of the cap. available_parallelism honours the cgroup quota, so the one-CPU cap means the server starts exactly one worker thread rather than one per host core; its own startup log says 1 workers.