How to validate email addresses in PHP
PHP has two built-in helpers for email validation, filter_var and checkdnsrr. They catch malformed addresses and dead domains. They don't catch disposable inboxes, typos like gmial.com, or mailboxes that don't exist. This guide covers the built-ins, their limits, and how to add the rest with one API call, including a ready-made Laravel rule.
1. Syntax with filter_var
<?php
function check_syntax(string $address): ?string
{
$address = trim($address);
if (filter_var($address, FILTER_VALIDATE_EMAIL) === false) {
return null;
}
[$local, $domain] = explode('@', $address, 2); // filter_var allows exactly one unquoted @
return $local . '@' . strtolower($domain);
}
var_dump(check_syntax('Jane.Doe@Example.COM')); // "Jane.Doe@example.com"
var_dump(check_syntax('jane@@example')); // NULLFILTER_VALIDATE_EMAIL rejects internationalized addresses (jos茅@example.com) unless you add FILTER_FLAG_EMAIL_UNICODE. Decide which you want; most US-focused sites can leave it off.
2. Mail server with checkdnsrr
<?php
function domain_accepts_mail(string $domain): bool
{
// MX record, or (per RFC 5321) an A/AAAA record when there's no MX.
// The trailing dot stops the resolver from appending your server's own search domain.
$fqdn = rtrim($domain, '.') . '.';
return checkdnsrr($fqdn, 'MX') || checkdnsrr($fqdn, 'A') || checkdnsrr($fqdn, 'AAAA');
}
var_dump(domain_accepts_mail('gmail.com')); // true
var_dump(domain_accepts_mail('gmial.con')); // falseTwo limits: checkdnsrr returns false both for "no such domain" and for a DNS timeout, so a slow resolver can make a real address look bad; and it doesn't recognize a null MX (0 ., "this domain takes no mail").
3. What the built-ins miss
- Disposable inboxes:
@mailinator.compasses both checks. You'd need a list of ~10,000 domains and keep it updated (more on that). - Typos:
jane@gmial.comhas valid syntax, and gmial.com has MX records, so both checks pass. - Missing mailboxes:
jnae.doe@gmail.compasses. Checking the mailbox means talking SMTP to Gmail on port 25, which most web hosts block, and which gets your server's IP blocklisted if done carelessly.
4. The full check with one API call
<?php
/** Verified.net email check. Returns the decoded result, or null if unreachable (fail open). */
function check_email(string $address): ?array
{
$ch = curl_init('https://api.verified.net/v1/email');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('VERIFIED_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['address' => $address]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($body === false || $status !== 200) {
return null; // 402 = out of credits, 429 = rate limited
}
return json_decode($body, true);
}
$result = check_email('jane.doe@gmial.com');
echo $result['recommendation']['action']; // review
echo $result['did_you_mean']; // jane.doe@gmail.comThe result's recommendation.action is accept, review or reject, with reasons such as disposable, no_mail_server, mailbox_not_found, catch_all, role_account or possible_typo. 1 credit per check; invalid syntax and repeats within 7 days are free. Fields: API docs.
5. A Laravel validation rule
For Laravel 10 and later. Put your key in .env as VERIFIED_KEY and add 'verified' => ['key' => env('VERIFIED_KEY')] to config/services.php.
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Http;
class DeliverableEmail implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
try {
$response = Http::withToken(config('services.verified.key'))
->timeout(10)
->post('https://api.verified.net/v1/email', ['address' => $value]);
} catch (\Throwable $e) {
return; // unreachable: don't block the form
}
if (! $response->ok()) {
return;
}
$result = $response->json();
if (! empty($result['did_you_mean'])) {
$fail("Did you mean {$result['did_you_mean']}?");
} elseif (($result['recommendation']['action'] ?? null) === 'reject') {
$fail('Please use a working, permanent email address.');
}
}
}$request->validate([
'email' => ['required', 'email', new \App\Rules\DeliverableEmail],
]);Tips
- Run the cheap checks first. Laravel's
emailrule (orfilter_var) before the API call means junk input never costs a request. - Fail open on errors and timeouts, as above.
- WordPress site? See stopping fake signups in WordPress.
Try it on your own data
Check an address or number now with the free checker, no signup needed, or start a 7-day trial and call the API from your code.
Start free trial API docs