# PHP SDK

URL: https://email-validation.org/docs/sdks/php/
Updated: 2026-10-23

> Documentation index: https://email-validation.org/llms.txt

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](https://email-validation.org/docs/code-samples/), which
call the API directly.

## Install

```bash
composer require email-validation/sdk
```

The namespace is `EmailValidation`.

## Validate an address

```php
<?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 send
```

Named 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](https://email-validation.org/docs/api/validate/#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](https://email-validation.org/docs/rate-limits/), or use a bulk job for lists.

## Handle errors

```php
<?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
<?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
<?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
<?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
<?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](https://email-validation.org/docs/statuses/) and
[test addresses](https://email-validation.org/docs/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).
