Email Verification with PHP: Complete API Integration Guide

PHP's filter_var($email, FILTER_VALIDATE_EMAIL) tells you if an email address looks right. That's it. It can't tell you if the mailbox exists, if the domain accepts mail, or if the address is a disposable throwaway that'll bounce in 24 hours. For that, you need real email verification through an API that actually connects to the mail server and checks.

This guide gives you production-ready PHP code for integrating the Bulk Email Checker API into your applications. You'll get working examples for single email checks, real-time form validation, bulk CSV processing with concurrent requests, and automated cron-based list cleaning. Every snippet uses the official API endpoint and handles errors properly - no shortcuts.

Why filter_var() Isn't Enough

Let's be clear about the difference between validation and verification. PHP's built-in filter_var() performs syntax validation - it checks if the string follows the email format rules defined in RFC 5322. That's useful for catching typos like missing @ signs or spaces in the address.

But filter_var() will happily pass nobody@doesnotexist12345.com. The format is valid. The mailbox? Completely fictional. Sending to addresses like that is how you rack up hard bounces and destroy your sender reputation.

📊
Key Stat: Email databases degrade by roughly 22-30% annually due to job changes, abandoned accounts, and domain expirations. Syntax validation alone can't detect this decay.

Email verification goes further. It checks DNS records, confirms MX server existence, and performs an SMTP handshake to verify the mailbox is real and accepting mail. Here's how the two compare:

Check filter_var() API Verification
Syntax format Yes Yes
Domain exists No Yes
MX records valid No Yes
Mailbox exists (SMTP) No Yes
Disposable email detection No Yes
Catch-all domain detection No Yes
Role-based email detection No Yes
Typo suggestions No Yes

Bottom line: use filter_var() as a quick client-side pre-filter. Use API verification before you store, import, or send to any email address.

Prerequisites and API Setup

What You Need

Before writing any code, make sure you have:

  1. PHP 7.4 or higher with the cURL extension enabled (most hosting environments include this by default)
  2. A Bulk Email Checker account - sign up at BulkEmailChecker.com and grab your API key from the dashboard
  3. Credits or an unlimited plan - pay-as-you-go pricing starts at $0.001 per verification with credits that never expire, or choose an unlimited API plan for high-volume needs

You can test with 10 free verifications daily before committing to any plan. No credit card required.

API Endpoint Overview

The Bulk Email Checker API is a simple REST endpoint. Send a GET request with your API key and the email address, and you get back a JSON response with the verification result. That's it - no SDK to install, no Composer package required, no authentication tokens to manage.

API Endpoint
https://api.bulkemailchecker.com/real-time/?key={API_KEY}&email={EMAIL}

The API returns JSON by default. If you need XML, add &xml=true to the query string.

Single Email Verification

Here's the simplest possible integration - a reusable function that verifies one email address and returns the parsed result. Drop this into any PHP project.

PHP
<?php

function verifyEmail(string $email, string $apiKey): ?array
{
    // Quick syntax check before hitting the API
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        return ['status' => 'failed', 'event' => 'invalid_syntax', 'details' => 'Email failed syntax validation'];
    }

    $url = 'https://api.bulkemailchecker.com/real-time/?key=' . $apiKey . '&email=' . urlencode($email);

    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_USERAGENT      => 'BEC-PHP-Integration/1.0',
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error    = curl_error($ch);
    curl_close($ch);

    if ($response === false || $httpCode !== 200) {
        error_log("Email verification failed for {$email}: {$error} (HTTP {$httpCode})");
        return null;
    }

    $result = json_decode($response, true);

    if (json_last_error() !== JSON_ERROR_NONE) {
        error_log("Invalid JSON from verification API: " . json_last_error_msg());
        return null;
    }

    return $result;
}

// Usage
$apiKey = 'YOUR_API_KEY';
$result = verifyEmail('test@example.com', $apiKey);

if ($result && $result['status'] === 'passed') {
    echo "Valid email - mailbox exists\n";
} elseif ($result && $result['status'] === 'failed') {
    echo "Invalid email: " . $result['event'] . "\n";
} else {
    echo "Could not determine - treat as unknown\n";
}

?>
💡
Pro Tip: Always run filter_var() before making the API call. It costs you nothing, catches obvious syntax problems instantly, and saves an API credit for emails that are clearly malformed.

Real-Time Form Validation

The most common use case for PHP developers is validating emails at the point of entry - signup forms, contact forms, checkout pages. Here's how to integrate email verification into a standard PHP form handler with AJAX support.

Backend Verification Endpoint

Create a PHP file that accepts an email via POST and returns the verification result as JSON. Your frontend JavaScript can call this endpoint as the user types or on form submission.

PHP - verify-endpoint.php
<?php
header('Content-Type: application/json');

// Only accept POST requests
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    echo json_encode(['error' => 'Method not allowed']);
    exit;
}

$email  = trim($_POST['email'] ?? '');
$apiKey = 'YOUR_API_KEY'; // Store this in an env variable in production

if (empty($email)) {
    echo json_encode(['valid' => false, 'message' => 'Email is required']);
    exit;
}

if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    echo json_encode(['valid' => false, 'message' => 'Invalid email format']);
    exit;
}

// Call the Bulk Email Checker API
$result = verifyEmail($email, $apiKey);

if ($result === null) {
    // API unreachable - fail open or closed based on your needs
    echo json_encode(['valid' => true, 'message' => 'Could not verify - accepted provisionally']);
    exit;
}

$response = [
    'valid'         => ($result['status'] === 'passed'),
    'status'        => $result['status'],
    'event'         => $result['event'] ?? '',
    'isDisposable'  => $result['isDisposable'] ?? false,
    'isRoleAccount' => $result['isRoleAccount'] ?? false,
    'isFreeService' => $result['isFreeService'] ?? false,
    'suggestion'    => $result['emailSuggested'] ?? null,
    'message'       => ''
];

// Build a human-readable message
if ($result['status'] === 'passed') {
    $response['message'] = 'Email verified successfully';
} elseif ($result['status'] === 'failed') {
    switch ($result['event']) {
        case 'mailbox_does_not_exist':
            $response['message'] = 'This mailbox doesn\'t exist';
            break;
        case 'domain_does_not_exist':
            $response['message'] = 'This domain doesn\'t exist';
            break;
        case 'invalid_syntax':
            $response['message'] = 'Invalid email format';
            break;
        default:
            $response['message'] = 'This email address is not valid';
    }
} else {
    $response['message'] = 'Could not fully verify this address';
}

// Offer typo correction if available
if (!empty($result['emailSuggested'])) {
    $response['message'] .= '. Did you mean ' . $result['emailSuggested'] . '?';
}

echo json_encode($response);

// Include the verifyEmail function from earlier
function verifyEmail(string $email, string $apiKey): ?array
{
    $url = 'https://api.bulkemailchecker.com/real-time/?key=' . $apiKey . '&email=' . urlencode($email);

    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_USERAGENT      => 'BEC-PHP-Integration/1.0',
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($response === false || $httpCode !== 200) {
        return null;
    }

    return json_decode($response, true);
}

?>

On the frontend, call this endpoint with a fetch request when the user finishes entering their email. You can debounce the call so it only fires after the user stops typing for 500ms - that way you don't burn credits on partial input.

Handling API Response Data

The Bulk Email Checker API returns a rich JSON response with far more than just pass/fail. Here's what you get back and how to use each field in your application logic.

Primary Status Values

Every response includes a status field with one of three values:

  • passed - the email is valid and the mailbox exists. Safe to send.
  • failed - the email is invalid. Don't send to it. Check the event field for the specific reason.
  • unknown - can't determine with certainty. This usually means the domain is a catch-all server or greylisting is active. Handle with caution.

Event Codes and What They Mean

The event field tells you exactly why an email passed, failed, or returned unknown. Here's a practical PHP function that maps events to actions:

PHP
<?php

function classifyEmail(array $result): string
{
    // Determine the action: accept, reject, or review
    if ($result['status'] === 'passed') {
        // Valid, but flag risky subtypes
        if ($result['isDisposable'] === true) {
            return 'reject'; // Throwaway email - will bounce soon
        }
        if ($result['isRoleAccount'] === true) {
            return 'review'; // info@, admin@ - might be OK, depends on use case
        }
        return 'accept';
    }

    if ($result['status'] === 'failed') {
        return 'reject'; // Hard no - mailbox doesn't exist, domain is dead, etc.
    }

    // Status is 'unknown' - catch-all, greylisting, or inconclusive
    switch ($result['event'] ?? '') {
        case 'is_catchall':
            return 'review'; // Domain accepts everything - risky but maybe valid
        case 'is_greylisting':
            return 'review'; // Server asked us to retry - might be valid
        case 'inconclusive':
            return 'review'; // Temporary issue - retry later
        default:
            return 'review';
    }
}

// Usage with your verification result
$result = verifyEmail('user@example.com', $apiKey);
$action = classifyEmail($result);

switch ($action) {
    case 'accept':
        // Add to database, allow signup, etc.
        saveToDatabase($result['email']);
        break;
    case 'reject':
        // Block the submission with a clear message
        showError('Please enter a valid email address');
        break;
    case 'review':
        // Accept but flag for manual review or re-verification later
        saveToDatabase($result['email'], ['needs_review' => true]);
        break;
}

?>

Boolean Detection Fields

Beyond status and event, the API returns several boolean fields that help you make smarter decisions about each address. The isDisposable flag catches temporary email services like Guerrilla Mail and Mailinator. The isFreeService flag identifies Gmail, Yahoo, Outlook, and similar free providers - useful if you're running B2B campaigns where you only want corporate addresses. And isGibberish catches randomly generated email addresses that spammers and bots tend to use.

⚠️
Warning: Don't automatically reject all role-based emails (info@, support@, sales@). Some legitimate business contacts only use role addresses. Flag them for review instead of outright blocking.

MX Enrichment Data

Every verification response includes free MX enrichment data in the mxEnrichment object. This tells you the mail server IP, hostname, geographic location, and ISP. You can use this data for analytics, geographic segmentation, or identifying which email providers your users prefer.

Bulk CSV Verification with Concurrent Requests

For cleaning existing email lists, you need to process thousands of addresses efficiently. PHP's curl_multi functions let you fire off multiple API requests simultaneously rather than waiting for each one to complete before starting the next.

This script reads a CSV file, verifies each email with up to 10 concurrent requests, and outputs a cleaned CSV with verification results.

PHP - bulk-verify.php
<?php

/**
 * Bulk CSV Email Verification
 * Reads a CSV, verifies emails concurrently, writes results to new CSV
 *
 * Usage: php bulk-verify.php input.csv output.csv
 */

$apiKey       = 'YOUR_API_KEY';
$concurrency  = 10;  // Max simultaneous requests
$inputFile    = $argv[1] ?? 'emails.csv';
$outputFile   = $argv[2] ?? 'verified_emails.csv';
$emailColumn  = 0;   // Column index containing email addresses (0-based)

if (!file_exists($inputFile)) {
    die("Input file not found: {$inputFile}\n");
}

// Read all emails from CSV
$emails = [];
$headers = null;
$handle = fopen($inputFile, 'r');

while (($row = fgetcsv($handle)) !== false) {
    if ($headers === null) {
        $headers = $row;
        continue;
    }
    $emails[] = $row;
}
fclose($handle);

echo "Loaded " . count($emails) . " emails for verification\n";

// Process in batches
$results = [];
$batches = array_chunk($emails, $concurrency);
$processed = 0;

foreach ($batches as $batch) {
    $multiHandle = curl_multi_init();
    $handles = [];

    // Create a cURL handle for each email in this batch
    foreach ($batch as $index => $row) {
        $email = trim($row[$emailColumn]);

        if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $results[] = array_merge($row, ['failed', 'invalid_syntax', 'N/A']);
            continue;
        }

        $url = 'https://api.bulkemailchecker.com/real-time/?key=' . $apiKey . '&email=' . urlencode($email);
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL            => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 30,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_USERAGENT      => 'BEC-PHP-BulkVerify/1.0',
        ]);

        curl_multi_add_handle($multiHandle, $ch);
        $handles[] = ['handle' => $ch, 'row' => $row];
    }

    // Execute all handles simultaneously
    $running = null;
    do {
        curl_multi_exec($multiHandle, $running);
        curl_multi_select($multiHandle);
    } while ($running > 0);

    // Collect results
    foreach ($handles as $item) {
        $response = curl_multi_getcontent($item['handle']);
        $data = json_decode($response, true);

        if ($data) {
            $results[] = array_merge($item['row'], [
                $data['status'] ?? 'error',
                $data['event'] ?? 'unknown',
                ($data['isDisposable'] ?? false) ? 'Yes' : 'No'
            ]);
        } else {
            $results[] = array_merge($item['row'], ['error', 'api_failure', 'N/A']);
        }

        curl_multi_remove_handle($multiHandle, $item['handle']);
        curl_close($item['handle']);
    }

    curl_multi_close($multiHandle);

    $processed += count($batch);
    echo "Processed {$processed}/" . count($emails) . " emails\n";

    // Small delay between batches to respect rate limits
    usleep(200000); // 200ms
}

// Write results to output CSV
$outHandle = fopen($outputFile, 'w');
fputcsv($outHandle, array_merge($headers, ['Status', 'Event', 'Disposable']));

foreach ($results as $row) {
    fputcsv($outHandle, $row);
}
fclose($outHandle);

echo "Done! Results saved to {$outputFile}\n";

// Print summary
$passed  = count(array_filter($results, fn($r) => end($r) !== 'N/A' && $r[count($r) - 3] === 'passed'));
$failed  = count(array_filter($results, fn($r) => $r[count($r) - 3] === 'failed'));
$unknown = count(array_filter($results, fn($r) => $r[count($r) - 3] === 'unknown'));

echo "\nSummary:\n";
echo "  Passed:  {$passed}\n";
echo "  Failed:  {$failed}\n";
echo "  Unknown: {$unknown}\n";

?>

Run it from the command line:

Bash
php bulk-verify.php my_email_list.csv cleaned_list.csv
💡
Pro Tip: For lists over 50,000 emails, consider using Bulk Email Checker's dashboard upload instead of the real-time API. Upload your CSV directly at BulkEmailChecker.com for faster processing with built-in deduplication and reporting.

Automated List Cleaning with Cron

Email lists decay at roughly 2% per month. If you're not cleaning regularly, you're accumulating dead addresses that drag down your deliverability. Here's a PHP script designed to run as a cron job, re-verifying addresses that haven't been checked in the last 90 days.

PHP - cron-reverify.php
<?php

/**
 * Cron-based email re-verification
 * Checks stale records and updates their verification status
 *
 * Crontab: 0 3 * * 1  php /path/to/cron-reverify.php
 * (runs every Monday at 3:00 AM)
 */

$apiKey    = 'YOUR_API_KEY';
$batchSize = 500;   // Process this many per run
$staleAge  = 90;    // Days since last verification

// Connect to your database
$pdo = new PDO('mysql:host=localhost;dbname=yourdb', 'user', 'pass', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);

// Grab emails that haven't been verified in X days
$stmt = $pdo->prepare("
    SELECT id, email 
    FROM subscribers 
    WHERE last_verified < DATE_SUB(NOW(), INTERVAL :days DAY)
       OR last_verified IS NULL
    ORDER BY last_verified ASC
    LIMIT :batch
");
$stmt->bindValue(':days', $staleAge, PDO::PARAM_INT);
$stmt->bindValue(':batch', $batchSize, PDO::PARAM_INT);
$stmt->execute();

$staleEmails = $stmt->fetchAll(PDO::FETCH_ASSOC);

if (empty($staleEmails)) {
    echo "No stale emails to verify. All clean!\n";
    exit;
}

echo "Found " . count($staleEmails) . " emails needing re-verification\n";

$updateStmt = $pdo->prepare("
    UPDATE subscribers 
    SET verification_status = :status,
        verification_event = :event,
        is_disposable = :disposable,
        last_verified = NOW()
    WHERE id = :id
");

$stats = ['passed' => 0, 'failed' => 0, 'unknown' => 0, 'errors' => 0];

foreach ($staleEmails as $record) {
    $result = verifyEmail($record['email'], $apiKey);

    if ($result === null) {
        $stats['errors']++;
        continue;
    }

    $updateStmt->execute([
        ':status'     => $result['status'],
        ':event'      => $result['event'] ?? '',
        ':disposable' => ($result['isDisposable'] ?? false) ? 1 : 0,
        ':id'         => $record['id']
    ]);

    $stats[$result['status']]++;

    // Rate limit - 200ms between requests
    usleep(200000);
}

// Log results
$summary = sprintf(
    "Re-verification complete: %d passed, %d failed, %d unknown, %d errors",
    $stats['passed'], $stats['failed'], $stats['unknown'], $stats['errors']
);
echo $summary . "\n";
error_log($summary);

// Include the verifyEmail function
function verifyEmail(string $email, string $apiKey): ?array
{
    $url = 'https://api.bulkemailchecker.com/real-time/?key=' . $apiKey . '&email=' . urlencode($email);

    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_USERAGENT      => 'BEC-PHP-Cron/1.0',
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($response === false || $httpCode !== 200) {
        return null;
    }

    return json_decode($response, true);
}

?>

Add this to your crontab to run weekly. Adjust the $batchSize based on your API plan and list size - 500 per week works well for lists under 50,000, keeping your verification costs predictable at just $0.50 per run.

Error Handling and Retry Logic

Production integrations need to handle failures gracefully. Network timeouts, rate limits, and temporary API issues happen. Here's a wrapper function with exponential backoff retry logic.

PHP
<?php

function verifyEmailWithRetry(string $email, string $apiKey, int $maxRetries = 3): ?array
{
    for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
        $result = verifyEmail($email, $apiKey);

        if ($result !== null) {
            return $result;
        }

        // Exponential backoff: 1s, 2s, 4s
        $delay = pow(2, $attempt - 1);
        error_log("Verification attempt {$attempt} failed for {$email}. Retrying in {$delay}s...");
        sleep($delay);
    }

    error_log("All {$maxRetries} verification attempts failed for {$email}");
    return null;
}

?>

For form integrations where you can't keep users waiting through retries, set a strict timeout and fall back gracefully. Accept the email provisionally and re-verify it asynchronously with a background process.

Action Required: Always store your API key in an environment variable or config file outside your web root. Never hardcode it in files accessible via HTTP.

Caching Results to Save Credits

If you're verifying the same addresses repeatedly (like on a login page or repeat form submissions), cache the results. A simple file-based cache or database lookup prevents redundant API calls.

PHP
<?php

function verifyEmailCached(string $email, string $apiKey, int $cacheTTL = 86400): ?array
{
    $cacheDir  = sys_get_temp_dir() . '/bec_cache';
    $cacheFile = $cacheDir . '/' . md5(strtolower($email)) . '.json';

    if (!is_dir($cacheDir)) {
        mkdir($cacheDir, 0755, true);
    }

    // Check cache first
    if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $cacheTTL) {
        $cached = json_decode(file_get_contents($cacheFile), true);
        if ($cached) {
            return $cached;
        }
    }

    // Cache miss - call the API
    $result = verifyEmail($email, $apiKey);

    if ($result !== null) {
        file_put_contents($cacheFile, json_encode($result), LOCK_EX);
    }

    return $result;
}

// Cache for 24 hours (86400 seconds) by default
$result = verifyEmailCached('user@example.com', $apiKey);

?>

Set the cache TTL based on your needs. 24 hours works for signup forms. For scheduled list cleaning, you might skip the cache entirely since the whole point is getting fresh results.

Frequently Asked Questions

How long does each API verification take?

Most verifications complete in under 2 seconds. The API performs a full SMTP handshake with the recipient's mail server in real-time, so response times depend partly on how fast that server responds. Set your cURL timeout to 30 seconds to handle edge cases with slow mail servers.

What PHP version do I need?

The code examples in this guide work with PHP 7.4 and above. The only extension requirement is cURL, which is enabled by default on virtually every PHP hosting environment. No Composer packages or external SDKs are needed - the API is a simple REST endpoint that works with plain cURL.

Should I fail open or fail closed when the API is unreachable?

It depends on your use case. For signup forms, failing open (accepting the email provisionally) usually provides the better user experience - then re-verify asynchronously. For bulk imports or high-risk operations, failing closed (rejecting unverified emails) is safer for your sender reputation.

How do I handle catch-all domains?

When the API returns status: unknown with event: is_catchall, it means the domain accepts all emails regardless of whether the specific mailbox exists. You can't definitively verify these. Accept them with a flag for monitoring, and track engagement metrics to identify which ones are actually active.

Can I use the unlimited API endpoint instead?

Yes. If you're on an unlimited API plan, just swap the endpoint from /real-time/ to /unlimited/:

PHP
// Pay-as-you-go (Real-time API)
$url = 'https://api.bulkemailchecker.com/real-time/?key=' . $apiKey . '&email=' . urlencode($email);

// Unlimited plan API
$url = 'https://api.bulkemailchecker.com/unlimited/?key=' . $apiKey . '&email=' . urlencode($email);

The response format is identical for both endpoints. Your code doesn't need any other changes.

Wrapping Up

You've now got everything you need to integrate email verification into any PHP application. The key takeaway: don't rely on filter_var() alone. It's a useful first filter, but real verification requires checking the mail server directly through the Bulk Email Checker API.

Start with single verification on your signup forms to stop bad addresses at the door. Then add bulk CSV processing for your existing lists and schedule a weekly cron job to catch addresses that decay over time. With pay-as-you-go pricing at $0.001 per verification and credits that never expire, there's no reason to keep sending to unverified addresses.

You can test everything in this guide right now with the free email checker - 10 free verifications daily, no signup required.

99.7% Accuracy Guarantee

Stop Bouncing. Start Converting.

Millions of emails verified daily. Industry-leading SMTP validation engine.