Developer Guide 12 min read

How to Block Disposable Emails in Laravel, Node.js, Python & PHP

Matt King
Matt King

April 21, 2026

How to Block Disposable Emails in Laravel, Node.js, Python & PHP

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

Frequently Asked Questions

What is the fastest way to block disposable emails?

Add a single API call to your signup form validation. Send the email address to Fidro's /v1/validate/email endpoint and check the disposable field in the response. If it returns true, reject the signup. The entire integration takes under 15 minutes in any framework.

Do I need a different integration for each framework?

No. The underlying API call is identical regardless of framework. You send a POST request with the email address and read the JSON response. The only thing that changes is where you put that check -- a Laravel validation rule, Express middleware, Django validator, or a plain function call.

Can I block disposable emails without an API?

You can maintain a static blocklist of known domains, but it goes stale within days. New disposable email services launch constantly, and many rotate domains to evade lists. An API checks against a continuously updated database of 50,000+ domains, which is the only reliable approach.

Will blocking disposable emails hurt my conversion rate?

In practice, no. Disposable email users almost never convert to paying customers. Blocking them removes noise from your funnel without losing real revenue. If you are concerned, use a soft block: allow the signup but flag the account for manual review or require phone verification.

How do I handle disposable emails from existing users?

Use Fidro's bulk email checker to scan your existing user base. Flag accounts registered with disposable addresses and send them a re-verification email asking for a permanent address. Most will not respond, confirming they were fake accounts all along.