How to block disposable email addresses at signup (Node.js, Python, PHP)
Disposable email services (yopmail, mailinator, temp-mail and hundreds of others) give anyone a working inbox in one click, with no account. They are handy for privacy, and they are the favorite tool of anyone farming your free tier: a new address for every new trial.
This post shows how to reject them at signup with one server-side call, in Node.js, Python and PHP, without breaking your signup when the check fails.
Where to check
Check on your backend, in the signup handler, before you create the account. A check in the browser can be skipped by anyone who calls your API directly, and it would expose your API key.
The flow:
- The user submits the form.
- Your backend sends the typed address to
POST https://api.signitize.io/v1/check. - If
disposableistrue, reject with a clear message. Otherwise continue with your normal signup.
The response also tells you whether the address can receive mail at all, and gives a canonical email for duplicate detection:
Disposable domains are deliverable: true: they do receive mail, which is what makes them useful for abuse. Check disposable, not deliverable, to catch them.
Node.js
async function checkEmail(email) {
try {
const response = await fetch('https://api.signitize.io/v1/check', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SIGNITIZE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(5000),
})
if (response.ok) return await response.json()
if (response.status === 400) return { invalid: true }
} catch {
// timeout or network error: fall through
}
return null // unknown: don't block the user
}
const result = await checkEmail(req.body.email)
if (result?.invalid) return res.status(422).json({ error: 'Enter a valid email address.' })
if (result?.disposable) return res.status(422).json({ error: 'Please use a permanent email address.' })
if (result && !result.deliverable) return res.status(422).json({ error: 'This email address cannot receive mail.' })
Python
import os
import requests
def check_email(email):
try:
response = requests.post(
"https://api.signitize.io/v1/check",
headers={"Authorization": f"Bearer {os.environ['SIGNITIZE_API_KEY']}"},
json={"email": email},
timeout=5,
)
except requests.RequestException:
return None # unknown: don't block the user
if response.ok:
return response.json()
if response.status_code == 400:
return {"invalid": True}
return None
result = check_email(typed_email)
if result and result.get("invalid"):
raise ValueError("Enter a valid email address.")
if result and result["disposable"]:
raise ValueError("Please use a permanent email address.")
if result and not result["deliverable"]:
raise ValueError("This email address cannot receive mail.")
PHP
function checkEmail(string $email): ?array
{
$curl = curl_init('https://api.signitize.io/v1/check');
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('SIGNITIZE_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['email' => $email]),
]);
$body = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
if ($body === false) return null; // timeout or network error: don't block the user
if ($status === 200) return json_decode($body, true);
if ($status === 400) return ['invalid' => true];
return null;
}
$result = checkEmail($_POST['email']);
if ($result && !empty($result['invalid'])) exit('Enter a valid email address.');
if ($result && $result['disposable']) exit('Please use a permanent email address.');
if ($result && !$result['deliverable']) exit('This email address cannot receive mail.');
Using Laravel or Django? The integration guides have a validation rule for each.
The rule that matters most: fail open
Every example above returns "unknown" when the check doesn't answer, and the signup continues. That is on purpose. If an email check can stop your signup, then any outage or network problem between your server and the API takes your signup form down with it.
Failing open doesn't mean skipping the check. Create those accounts as pending, recheck them a few minutes later, and hold back the free credits until they pass. Never block a signup has the full pattern, including the recheck job.
Why not a blocklist file?
You can download a list of disposable domains from GitHub and check against it. It works on day one and gets worse every week, because throwaway services register new domains constantly. We wrote about this in why disposable email blocklists go out of date.
Signitize combines a list with DNS signals, so a domain registered yesterday can still be caught, and every newly detected domain is remembered for the next check. The details are in Detection.
Write a helpful error message
The person behind a disposable address may be a real user who just wants privacy. Tell them what to do:
Disposable email addresses are not allowed. Please use a permanent email address.
If you want to keep privacy-minded users, note that relay services such as Firefox Relay, DuckDuckGo Email Protection and Apple's Hide My Email forward to a real inbox. Signitize returns disposable: false for them. Whether to block disposable emails at all is a real trade-off; we cover it in Should you block disposable emails?
Get a free API key: 2,500 checks a month, no credit card.