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.
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:
- PHP 7.4 or higher with the cURL extension enabled (most hosting environments include this by default)
- A Bulk Email Checker account - sign up at BulkEmailChecker.com and grab your API key from the dashboard
- 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.
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
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";
}
?>
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
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
eventfield 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
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.
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 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:
php bulk-verify.php my_email_list.csv cleaned_list.csv
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-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
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.
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
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/:
// 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.
Stop Bouncing. Start Converting.
Millions of emails verified daily. Industry-leading SMTP validation engine.