How to Block Disposable Emails in Laravel, Node.js, Python & PHP
April 21, 2026
Disposable emails are throwaway addresses from services like Mailinator, Guerrilla Mail, and Temp Mail. Users create them in seconds to exploit free tiers, bypass verification, and create duplicate accounts. If you accept email signups, you need to block them.
This guide gives you copy-paste code for every major backend framework. Each example calls Fidro's validation API to check whether an email is disposable, then blocks or flags the signup accordingly.
The API Call (All Frameworks)
Every integration below uses the same API call. Send the email to Fidro's endpoint and read the response:
curl -X POST https://api.fidro.io/v1/validate/email \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "user@mailinator.com"}'
Response:
{
"email": "user@mailinator.com",
"disposable": true,
"risk_score": 0.92,
"domain": {
"name": "mailinator.com",
"mx_valid": true,
"age_days": 7300
},
"recommendation": "block"
}
The disposable field is the one you care about. If it is true, the email is from a known throwaway provider. The risk_score (0 to 1) and recommendation give you additional context for borderline cases.
Laravel
Laravel's custom validation rules are the cleanest way to integrate. For a full deep dive with caching, queue fallbacks, and testing patterns, see our complete Laravel email validation guide. Here is the quick version.
Create the validation rule
php artisan make:rule NotDisposableEmail
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Http;
class NotDisposableEmail implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$response = Http::withToken(config('services.fidro.key'))
->post('https://api.fidro.io/v1/validate/email', [
'email' => $value,
]);
if ($response->successful() && $response->json('disposable')) {
$fail('Disposable email addresses are not allowed.');
}
}
}
Use it in your registration controller
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users', new NotDisposableEmail],
'password' => ['required', 'confirmed', 'min:8'],
]);
That is it. Every signup attempt with a disposable email now gets rejected with a clear error message. The rule works with Jetstream, Breeze, Fortify, or any custom auth flow.
Add caching to avoid repeat API calls
Wrap the HTTP call in a cache check so the same domain is not validated twice:
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$domain = str($value)->after('@')->lower()->toString();
$isDisposable = cache()->remember(
"fidro:disposable:{$domain}",
now()->addHours(24),
function () use ($value) {
$response = Http::withToken(config('services.fidro.key'))
->timeout(3)
->post('https://api.fidro.io/v1/validate/email', [
'email' => $value,
]);
return $response->successful() ? $response->json('disposable') : false;
}
);
if ($isDisposable) {
$fail('Disposable email addresses are not allowed.');
}
}
The 3-second timeout and false-on-failure ensure your signup form never breaks if the API is temporarily unreachable.
Node.js (Express)
Express middleware lets you protect every route that accepts an email. For the full middleware build with IP checking, risk scoring, and rate limiting, see our Node.js fraud prevention middleware guide.
Quick middleware
const axios = require('axios');
async function blockDisposableEmails(req, res, next) {
const email = req.body.email;
if (!email) return next();
try {
const { data } = await axios.post(
'https://api.fidro.io/v1/validate/email',
{ email },
{
headers: { Authorization: `Bearer ${process.env.FIDRO_API_KEY}` },
timeout: 3000,
}
);
if (data.disposable) {
return res.status(422).json({
error: 'Disposable email addresses are not allowed.',
});
}
} catch (err) {
// Fail open: allow the request if the API is unreachable
console.error('Fidro validation failed:', err.message);
}
next();
}
// Apply to your signup route
app.post('/api/signup', blockDisposableEmails, signupController);
With in-memory caching
const cache = new Map();
async function blockDisposableEmails(req, res, next) {
const email = req.body.email;
if (!email) return next();
const domain = email.split('@')[1]?.toLowerCase();
if (!domain) return next();
// Check cache first
if (cache.has(domain)) {
if (cache.get(domain)) {
return res.status(422).json({
error: 'Disposable email addresses are not allowed.',
});
}
return next();
}
try {
const { data } = await axios.post(
'https://api.fidro.io/v1/validate/email',
{ email },
{
headers: { Authorization: `Bearer ${process.env.FIDRO_API_KEY}` },
timeout: 3000,
}
);
// Cache for 24 hours
cache.set(domain, data.disposable);
setTimeout(() => cache.delete(domain), 86400000);
if (data.disposable) {
return res.status(422).json({
error: 'Disposable email addresses are not allowed.',
});
}
} catch (err) {
console.error('Fidro validation failed:', err.message);
}
next();
}
For production, swap the Map for Redis. The pattern is the same.
Python (Django)
Django's validator system makes this a one-liner in your model or form.
Create the validator
# validators.py
import requests
from django.conf import settings
from django.core.exceptions import ValidationError
def validate_not_disposable(email):
try:
response = requests.post(
"https://api.fidro.io/v1/validate/email",
json={"email": email},
headers={"Authorization": f"Bearer {settings.FIDRO_API_KEY}"},
timeout=3,
)
response.raise_for_status()
data = response.json()
if data.get("disposable"):
raise ValidationError(
"Disposable email addresses are not allowed.",
code="disposable_email",
)
except requests.RequestException:
# Fail open if the API is unreachable
pass
Add it to your User model
from django.db import models
from .validators import validate_not_disposable
class User(models.Model):
email = models.EmailField(
unique=True,
validators=[validate_not_disposable],
)
Or add it to a form directly:
from django import forms
from .validators import validate_not_disposable
class SignupForm(forms.Form):
email = forms.EmailField(validators=[validate_not_disposable])
password = forms.CharField(widget=forms.PasswordInput)
Add caching with Django's cache framework
from django.core.cache import cache as django_cache
def validate_not_disposable(email):
domain = email.rsplit("@", 1)[-1].lower()
cache_key = f"fidro:disposable:{domain}"
cached = django_cache.get(cache_key)
if cached is not None:
if cached:
raise ValidationError(
"Disposable email addresses are not allowed.",
code="disposable_email",
)
return
try:
response = requests.post(
"https://api.fidro.io/v1/validate/email",
json={"email": email},
headers={"Authorization": f"Bearer {settings.FIDRO_API_KEY}"},
timeout=3,
)
response.raise_for_status()
data = response.json()
is_disposable = data.get("disposable", False)
django_cache.set(cache_key, is_disposable, timeout=86400)
if is_disposable:
raise ValidationError(
"Disposable email addresses are not allowed.",
code="disposable_email",
)
except requests.RequestException:
pass
Python (Flask)
Flask does not have a built-in validator system, so a decorator works well.
import functools
import requests
from flask import request, jsonify, current_app
def block_disposable_emails(f):
@functools.wraps(f)
def decorated(*args, **kwargs):
email = request.json.get("email", "") if request.is_json else ""
if not email:
return f(*args, **kwargs)
try:
resp = requests.post(
"https://api.fidro.io/v1/validate/email",
json={"email": email},
headers={
"Authorization": f"Bearer {current_app.config['FIDRO_API_KEY']}"
},
timeout=3,
)
resp.raise_for_status()
if resp.json().get("disposable"):
return jsonify(
{"error": "Disposable email addresses are not allowed."}
), 422
except requests.RequestException:
pass
return f(*args, **kwargs)
return decorated
# Usage
@app.route("/signup", methods=["POST"])
@block_disposable_emails
def signup():
# Your signup logic here
pass
PHP (No Framework)
If you are running plain PHP without a framework, a simple function is all you need.
<?php
function isDisposableEmail(string $email): bool
{
$ch = curl_init('https://api.fidro.io/v1/validate/email');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['email' => $email]),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $_ENV['FIDRO_API_KEY'],
'Content-Type: application/json',
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 3,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || !$response) {
return false; // Fail open
}
$data = json_decode($response, true);
return !empty($data['disposable']);
}
Use it in your signup handler
<?php
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if (!$email) {
http_response_code(422);
echo json_encode(['error' => 'Invalid email address.']);
exit;
}
if (isDisposableEmail($email)) {
http_response_code(422);
echo json_encode(['error' => 'Disposable email addresses are not allowed.']);
exit;
}
// Continue with registration...
With file-based caching
If you do not have Redis or Memcached available, a simple file cache works:
function isDisposableEmail(string $email): bool
{
$domain = strtolower(explode('@', $email)[1] ?? '');
if (!$domain) return false;
$cacheFile = sys_get_temp_dir() . '/fidro_' . md5($domain) . '.cache';
// Check cache (24-hour TTL)
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < 86400) {
return (bool) file_get_contents($cacheFile);
}
$ch = curl_init('https://api.fidro.io/v1/validate/email');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['email' => $email]),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $_ENV['FIDRO_API_KEY'],
'Content-Type: application/json',
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 3,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || !$response) {
return false;
}
$data = json_decode($response, true);
$isDisposable = !empty($data['disposable']);
file_put_contents($cacheFile, $isDisposable ? '1' : '0');
return $isDisposable;
}
Choosing Your Blocking Strategy
The code above uses a hard block: reject the signup immediately. That is the right default for most products, but you have options:
| Strategy | When to use | User experience |
|---|---|---|
| Hard block | B2B SaaS, paid products, anything with a free trial | Error message: "Please use a permanent email address" |
| Soft block | Consumer apps, marketplaces, community platforms | Allow signup but flag for review, limit access until verified |
| Friction increase | Products where false positives are costly | Require phone verification or credit card for flagged emails |
| Log only | During initial rollout to measure impact | No user-facing change, just track disposable signup volume |
Start with "log only" for a week to see how many disposable signups you are getting. Then switch to hard block. You can always soften the policy later.
Handling Edge Cases
API timeouts
Every example above fails open: if the API is unreachable, the signup goes through. This is intentional. A 3-second timeout means your signup form never breaks, even during rare API downtime. You can catch these cases with a background re-validation job.
Custom domains that look disposable
Some companies run their own mail servers on uncommon domains. Fidro's risk scoring accounts for this by checking domain age, MX configuration, and usage patterns alongside the disposable database. A 2-year-old domain with proper MX records will not be flagged even if it is unfamiliar.
Existing users with disposable emails
Use the bulk email checker to scan your database. Export the results and decide your policy: force re-verification, restrict access, or simply flag the accounts in your analytics so they stop polluting your metrics.
What's Next
- Full Laravel integration guide with queue jobs, testing, and Jetstream/Breeze support
- Node.js fraud prevention middleware with IP checking and risk scoring
- Email validation API integration guide for advanced patterns
- Browse 5,000+ disposable domains to see what you are up against
- API documentation for the full endpoint reference