Adding Email Validation to Laravel: A Step-by-Step Guide
Matt King
October 1, 2025
Laravel does not include built-in disposable email detection. Its email validation rule only checks format and optionally DNS — it cannot identify throwaway providers, check domain reputation, or assign a risk score. For that, you need an external validation API.
This guide walks through a complete integration: service class, custom validation rule, caching, and async fallback.
What You'll Build
- A
FidroServiceclass that wraps the API calls - A custom
NotDisposableEmailvalidation rule - Redis caching to minimize API calls
- A queue job for async validation fallback
Step 1: Add Your API Key
# .env
FIDRO_API_KEY=your_api_key_here
// config/services.php
'fidro' => [
'api_key' => env('FIDRO_API_KEY'),
'base_url' => env('FIDRO_BASE_URL', 'https://api.fidro.io/v1'),
],
Step 2: Create the Service Class
<?php
// app/Services/FidroService.php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class FidroService
{
public function validateEmail(string $email): ?array
{
$cacheKey = 'fidro:email:' . md5($email);
return Cache::remember($cacheKey, now()->addHours(24), function () use ($email) {
try {
$response = Http::timeout(3)
->withHeaders([
'Authorization' => 'Bearer ' . config('services.fidro.api_key'),
])
->post(config('services.fidro.base_url') . '/validate/email', [
'email' => $email,
]);
if ($response->successful()) {
return $response->json();
}
Log::warning('Fidro API error', [
'status' => $response->status(),
'email' => $email,
]);
return null;
} catch (\Exception $e) {
Log::warning('Fidro API unreachable', [
'error' => $e->getMessage(),
'email' => $email,
]);
return null; // Fail open
}
});
}
public function isDisposable(string $email): bool
{
$result = $this->validateEmail($email);
return $result['disposable'] ?? false;
}
public function riskScore(string $email): float
{
$result = $this->validateEmail($email);
return $result['risk_score'] ?? 0.0;
}
}
Step 3: Create the Validation Rule
<?php
// app/Rules/NotDisposableEmail.php
namespace App\Rules;
use App\Services\FidroService;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class NotDisposableEmail implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$fidro = app(FidroService::class);
$result = $fidro->validateEmail($value);
// Fail open: if API is unreachable, allow the email
if ($result === null) {
return;
}
if ($result['disposable'] ?? false) {
$fail('Please use a permanent email address. Temporary emails are not accepted.');
}
if (($result['risk_score'] ?? 0) > 0.7) {
$fail('This email address could not be verified. Please try a different one.');
}
}
}
Step 4: Use It in Registration
// In your registration controller or form request
use App\Rules\NotDisposableEmail;
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users', new NotDisposableEmail],
'password' => ['required', 'confirmed', 'min:8'],
]);
This works identically with Laravel Breeze, Jetstream, and Fortify. Just add the rule to the existing validation array.
Step 5: Async Fallback with Queue Jobs
For cases where the API times out during signup, queue a background check:
<?php
// app/Jobs/ValidateUserEmail.php
namespace App\Jobs;
use App\Models\User;
use App\Services\FidroService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ValidateUserEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public User $user) {}
public function handle(FidroService $fidro): void
{
$result = $fidro->validateEmail($this->user->email);
if ($result && ($result['disposable'] ?? false)) {
$this->user->update([
'email_flagged' => true,
'email_flag_reason' => 'disposable',
]);
}
}
}
Dispatch it after registration:
ValidateUserEmail::dispatch($user)->delay(now()->addSeconds(5));
Testing the Integration
Mock the API in your tests so you never call the real endpoint:
public function test_disposable_emails_are_rejected(): void
{
Http::fake([
'api.fidro.io/*' => Http::response([
'email' => 'test@mailinator.com',
'disposable' => true,
'risk_score' => 0.9,
]),
]);
$response = $this->post('/register', [
'name' => 'Test User',
'email' => 'test@mailinator.com',
'password' => 'password123',
'password_confirmation' => 'password123',
]);
$response->assertSessionHasErrors('email');
}
Next Steps
- Get your free API key — 200 validations/month included
- Check the API documentation for the complete response schema
- Try the free email checker to test addresses interactively