PHP Email Validation: Syntax, MX Records, and One API Request
PHP’s filter_var(), an MX lookup, and an email verification API answer three different questions. Combining them in that order gives fast feedback without pretending that syntax or DNS confirms a mailbox.
FILTER_VALIDATE_EMAILrejects input outside PHP’s supported address shape.dns_get_record()shows whether explicit MX records are published.- One server-side API request returns the address status, action, and evidence.
The runnable example below keeps the project key outside the web root and renders an honest result for each layer.
Create the project
You need PHP 8 with the cURL extension. Create this structure:
php-email-check/
├── .env
└── public/
└── index.php
Create an emailverifier.dev account and project, copy the API key, and place it in .env:
EMAILVERIFIER_API_KEY=ev_your_project_key
Do not place .env inside public/ or commit it.
Build the one-file PHP form
Create public/index.php:
<?php
declare(strict_types=1);
function loadApiKey(string $path): string
{
if (!is_readable($path)) {
throw new RuntimeException('The .env file is missing.');
}
foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
if (str_starts_with($line, 'EMAILVERIFIER_API_KEY=')) {
return trim(substr($line, strlen('EMAILVERIFIER_API_KEY=')));
}
}
throw new RuntimeException('EMAILVERIFIER_API_KEY is missing.');
}
function verifyEmail(string $email, string $apiKey): array
{
$curl = curl_init('https://emailverifier.dev/api/v1/verify');
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_TIMEOUT => 5,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-API-Key: ' . $apiKey,
],
CURLOPT_POSTFIELDS => json_encode(
['email' => $email],
JSON_THROW_ON_ERROR
),
]);
$body = curl_exec($curl);
if ($body === false) {
throw new RuntimeException('Verification request failed: ' . curl_error($curl));
}
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
if ($status < 200 || $status >= 300) {
throw new RuntimeException('Verification service returned HTTP ' . $status);
}
return json_decode($body, true, flags: JSON_THROW_ON_ERROR);
}
$email = '';
$error = null;
$result = null;
$mxState = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = trim((string) ($_POST['email'] ?? ''));
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
$error = 'Enter an email address PHP can parse.';
} else {
$domain = substr($email, strrpos($email, '@') + 1);
$mxRecords = dns_get_record($domain, DNS_MX);
$mxState = $mxRecords === false
? 'DNS lookup unavailable'
: (count($mxRecords) > 0 ? 'Explicit MX records found' : 'No explicit MX records');
try {
$apiKey = loadApiKey(dirname(__DIR__) . '/.env');
$result = verifyEmail($email, $apiKey);
} catch (Throwable $exception) {
error_log($exception->getMessage());
$error = 'Email verification is temporarily unavailable. Try again.';
}
}
}
function e(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PHP email validation</title>
</head>
<body>
<main>
<h1>Check an email</h1>
<form method="post">
<label for="email">Email address</label>
<input id="email" name="email" type="email" required value="<?= e($email) ?>">
<button type="submit">Check email</button>
</form>
<?php if ($error): ?>
<p role="alert"><?= e($error) ?></p>
<?php endif; ?>
<?php if ($result): ?>
<h2>Result</h2>
<p>MX: <?= e((string) $mxState) ?></p>
<p>Status: <strong><?= e((string) $result['status']) ?></strong></p>
<p>Action: <strong><?= e((string) $result['action']) ?></strong></p>
<pre><?= e(json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) ?></pre>
<?php endif; ?>
</main>
</body>
</html>
Run the PHP development server from the project directory:
php -S localhost:8080 -t public
Open http://localhost:8080 and submit an address. Each valid submission makes one emailverifier.dev request.
Do not turn the MX line into a verdict
dns_get_record($domain, DNS_MX) returns explicit MX records when they exist. An empty array is not sufficient evidence that the domain cannot receive email because SMTP supports address-record fallback. A DNS error is also different from an empty answer.
The form displays the MX observation but lets the verification result own the signup decision. Use the free MX lookup when you need to inspect exchangers, priorities, or a null MX record manually.
Map the API result into PHP application logic
The four possible statuses remain separate from the three recommended actions:
| Status | Evidence | Typical action |
|---|---|---|
deliverable | Positive mailbox evidence was available. | Allow, unless another signal calls for review. |
risky | A disposable provider, likely typo, or another material risk was found. | Follow action and show a correction where possible. |
undeliverable | A confirmed syntax, routing, or mailbox failure was found. | Block. |
unknown | The mailbox could not be classified conclusively. | Review or continue with confirmation. |
A registration handler can branch directly:
if ($result['action'] === 'block') {
// Return a field error and do not create the user.
} elseif ($result['action'] === 'review') {
// Require confirmation or queue the account for review.
} else {
// Create the account after the rest of the signup checks pass.
}
filter_var() is still useful, but PHP documents it as a validation filter with a supported syntax subset. It does not prove that the domain or mailbox works. The PHP filter documentation and DNS record documentation describe the two local checks used above.