PHP has filter_var. Laravel has the email validation rule. Both confirm the address looks like an email. Neither confirms the mailbox exists. Here is the layered validation pattern we use in our own apps, with copy-paste code.
Layer 1: PHP filter_var
$email = trim($input);
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return ['error' => 'Invalid email format'];
}
Catches typos, missing TLDs, multiple @ signs. Takes microseconds. Run it first before any network calls.
Layer 2: Laravel's validation rule
Laravel ships several built-in email rules:
$request->validate([
'email' => ['required', 'email:rfc,dns', 'max:255'],
]);
The rfc variant uses Laravel's built-in syntax check. The dns variant adds a DNS lookup: it confirms the domain exists. Together they catch about 30% of bad emails without an external service.
Note: the dns check requires the egulias/email-validator package, which Laravel includes by default. It also adds 100 to 300 ms of latency per validation. For high-traffic signup endpoints, consider caching DNS results.
Layer 3: Real verification via the MailoClean API
Inside your Laravel controller or service:
use Illuminate\Support\Facades\Http;
class VerifyEmailService
{
public function verify(string $email): array
{
$response = Http::withToken(config('services.mailoclean.key'))
->acceptJson()
->timeout(5)
->post('https://mailoclean.com/api/v1/verify', [
'email' => $email,
]);
if ($response->failed()) {
return ['status' => 'unknown', 'score' => 50];
}
return $response->json();
}
}
Wiring it into a FormRequest
The cleanest pattern is a custom rule that hits the verification service.
// app/Rules/VerifiedEmail.php
namespace App\Rules;
use App\Services\VerifyEmailService;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class VerifiedEmail implements ValidationRule
{
public function __construct(private VerifyEmailService $service) {}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$result = $this->service->verify($value);
if (in_array($result['status'] ?? null, ['invalid', 'disposable'], true)) {
$fail('Please use a valid, permanent email address.');
}
}
}
Then in your FormRequest:
public function rules(): array
{
return [
'email' => ['required', 'email:rfc', new VerifiedEmail($this->container->make(VerifyEmailService::class))],
];
}
Caching
Verification costs one credit per call. If a user submits the same email twice in a session, you do not want to pay twice. Cache the result for 24 hours:
use Illuminate\Support\Facades\Cache;
public function verify(string $email): array
{
return Cache::remember("verify:{$email}", now()->addDay(), function () use ($email) {
return $this->callApi($email);
});
}
MailoClean's API also caches server-side, so repeated checks within 24h are free even without your own cache. Still, local caching saves the round-trip.
Configuration
Add to config/services.php:
'mailoclean' => [
'key' => env('MAILOCLEAN_KEY'),
],
And to your .env:
MAILOCLEAN_KEY=your-api-key-here
Handling failures gracefully
- If the API times out, accept the signup with a flag and reverify in a background job.
- If the API returns
unknown, accept by default. Do not block real users for verifier ambiguity. - Log every verification call. Auditing usage helps spot abuse and predict billing.
FAQ
Does Laravel have a built-in real verifier?
The email:dns rule checks domain existence but not mailbox existence. Real verification requires an external service.
Can I run verification in a queue?
Yes. Some signup flows verify asynchronously after account creation, then send a "please use a real email" message if it comes back invalid. Choose this when latency on the signup endpoint matters more than reject-at-the-door.
Get an API key
Read the docs, sign up, paste the code above. Five-minute integration.