PHP SDK
The official PHP SDK gives typed results (readonly classes), typed exceptions and automatic retries. It needs PHP 8.1 or later with the curl and json extensions and has no other dependencies.
Not published yet. The Composer package email-validation/sdk is not on Packagist yet, so the install step below does not work today. The SDK is complete and tested; it will be published once the registry account and the licence are in place. Until then, use the code samples, which call the API directly.
Install
composer require email-validation/sdkThe namespace is EmailValidation.
Validate an address
<?php
require __DIR__ . '/vendor/autoload.php';
use EmailValidation\Client;
$client = new Client(apiKey: (string) getenv('EV_API_KEY'));
$result = $client->validate('accepted@sandbox.email-validation.org');
echo $result->status, ' ', $result->reason, ' ', $result->action, PHP_EOL; // valid accepted sendNamed arguments: ipAddress, timeout (seconds the check may take, 5–15) and fresh. fresh: true checks the address again even when it was checked in the last 12 months (see repeat checks). The result has the API's fields as camelCase readonly properties (didYouMean, freeEmail, processedAt, …); toArray() and json_encode() give back the API's JSON. Compare values with the constants of Status, Reason and Action.
Async
The PHP client is synchronous. Run several checks at once with your framework's queues or workers, within your rate limit, or use a bulk job for lists.
Handle errors
<?php
use EmailValidation\Exception\EmailValidationException;
use EmailValidation\Exception\InsufficientCreditsException;
use EmailValidation\Exception\RateLimitException;
try {
$client->validate($address);
} catch (InsufficientCreditsException $e) {
// buy credits; nothing was charged
} catch (RateLimitException $e) {
echo 'retry in ', $e->retryAfter, ' seconds', PHP_EOL;
} catch (EmailValidationException $e) {
error_log(sprintf('%s (%d, request %s)', $e->errorCode, $e->status, $e->requestId ?? '-'));
}Every exception extends EmailValidation\Exception\EmailValidationException with status (0 when no answer arrived), errorCode, requestId and retryAfter. Classes: InvalidRequestException, AuthenticationException, InsufficientCreditsException, PermissionDeniedException, NotFoundException, ConflictException, RateLimitException, ServiceUnavailableException, ServerException and ConnectionException. Wrong arguments throw \InvalidArgumentException before anything is sent.
Retries and timeouts
<?php
$client = new Client(
apiKey: (string) getenv('EV_API_KEY'),
timeout: 30.0, // seconds per attempt
maxRetries: 2, // after 429 / 503 answers
maxRetryWait: 10.0, // longer Retry-After values are thrown to you instead of waited out
);429 and 503 answers are retried with backoff, honouring Retry-After. A daily key cap is never retried, and a validation is never retried after a network failure, because it may already have been charged.
Credits, usage and status
<?php
$credits = $client->credits(); // balance, debt, pending, lots
$usage = $client->usage(from: '2026-10-19', to: '2026-11-30', groupBy: 'day');
$status = $client->status();Bulk jobs
<?php
use EmailValidation\Model\BulkJob;
$job = $client->bulk->create(emails: ['accepted@sandbox.email-validation.org', 'catch_all@sandbox.email-validation.org']);
// or from a CSV file (up to 100 MB)
$job = $client->bulk->create(file: fopen('customers.csv', 'rb'), column: 'email', hasHeader: true);
$job = $client->bulk->wait($job->id, onProgress: function (BulkJob $job): void {
echo $job->status, ' ', $job->processed, PHP_EOL;
});
if ($job->status === 'completed') {
file_put_contents('valid-and-risky.csv', $client->bulk->results($job->id, filter: ['valid', 'risky']));
} elseif ($job->isStuckReady()) {
echo 'could not start: ', $job->errorCode, PHP_EOL; // e.g. insufficient_credits: buy credits, then start()
}wait() returns when the job is completed, failed, cancelled or expired, or when it is still ready with an errorCode: it could not start and does not move on by itself. Pass timeout: (seconds) to give up earlier. Also: get(), start() for jobs created with start: false (call it before wait()), cancel(), and resultsStream(), which returns the CSV as a stream for large files.
Verify webhooks
<?php
use EmailValidation\Exception\WebhookVerificationException;
use EmailValidation\Webhook;
$payload = file_get_contents('php://input'); // the raw body, before any JSON parsing
try {
$event = Webhook::verify($payload, getallheaders(), (string) getenv('EV_WEBHOOK_SECRET'));
} catch (WebhookVerificationException $e) {
http_response_code(400);
exit;
}
if ($event->type === 'bulk.completed') {
error_log('job done ' . $event->data['job_id']);
}
http_response_code(204);Headers can be getallheaders(), a PSR-7 getHeaders() array or $_SERVER. $event->type can also be webhook.test, sent when you press Send test in the dashboard: answer it with a 2xx like any other event. Webhook::sign() creates a valid signature for local tests.
Constants
Vocabulary holds STATUSES, REASONS, REASONS_BY_STATUS, REASON_INFO, ACTIONS, ERROR_CODES, WEBHOOK_EVENTS and Vocabulary::sandboxAddress($reason); Status, Reason, Action, BulkJobStatus and ErrorCode hold named constants. See statuses and reasons and test addresses.
Bring your own HTTP client
The built-in CurlTransport never follows redirects and accepts extra cURL options, such as a proxy: new Client(apiKey: …, transport: new CurlTransport([CURLOPT_PROXY => 'http://proxy.internal:3128'])). To replace it, for example in tests or with a PSR-18 client, implement EmailValidation\Http\TransportInterface (one send() method: a Request in, a Response out).