emailverifier.dev
Posts

PHP Email Validation: Syntax, MX Records, and One API Request

| 4 min read | Usama Ejaz
Simple PHP label and envelope for a PHP email validation tutorial.

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.

  1. FILTER_VALIDATE_EMAIL rejects input outside PHP’s supported address shape.
  2. dns_get_record() shows whether explicit MX records are published.
  3. 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:

StatusEvidenceTypical action
deliverablePositive mailbox evidence was available.Allow, unless another signal calls for review.
riskyA disposable provider, likely typo, or another material risk was found.Follow action and show a correction where possible.
undeliverableA confirmed syntax, routing, or mailbox failure was found.Block.
unknownThe 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.

Continue reading