VERIFIED.NET
Guides 路 Email

How to validate email addresses in PHP

Updated 2026-10-19 路 7 min read

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
<?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'));          // NULL

FILTER_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
<?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'));   // false

Two 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.com passes both checks. You'd need a list of ~10,000 domains and keep it updated (more on that).
  • Typos: jane@gmial.com has valid syntax, and gmial.com has MX records, so both checks pass.
  • Missing mailboxes: jnae.doe@gmail.com passes. 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
<?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.com

The 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 路 app/Rules/DeliverableEmail.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.');
        }
    }
}
php 路 in a controller or form request
$request->validate([
    'email' => ['required', 'email', new \App\Rules\DeliverableEmail],
]);

Tips

  • Run the cheap checks first. Laravel's email rule (or filter_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