# Spaw PHP SDK: verify emails from PHP

The official spaw/spaw-php package wraps every Spaw endpoint in a dependency-free client with one exception per API code. PHP 8.2 with curl and json.

Updated: 2026-09-03

The PHP client is a thin, dependency-free wrapper over the REST API described by the OpenAPI document: one method per endpoint, a `Spaw\Response` carrying the envelope's `data` and `meta` arrays exactly as the API sends them, and a typed exception for every error code. It needs PHP 8.2 with the curl and json extensions, and its HTTP transport is an interface you can replace to route requests through your own stack or fake the API in tests.

The package is built and will be published on Packagist as `spaw/spaw-php`. Until the listing is live, Laravel's `Http::withToken()` against the API reference does the same job in a few lines.

## Install and verify one address

1. Create a secret key on the API keys page and put it in your environment.
2. Install the package once it is published:

```bash
composer require spaw/spaw-php
```

3. Verify an address:

```php
use Spaw\Client;

$spaw = new Client(getenv('SPAW_API_KEY'));

$result = $spaw->verifyEmail('mia@acme.com');

$result->data['deliverable'];   // "deliverable" | "risky" | "undeliverable"
$result->data['reason'];        // null, or e.g. "disposable", "mailbox_not_found"
$result->data['risk_score'];    // 0–100, an auditable sum of published weights
$result->creditsUsed();         // 1 for a deliverable or risky answer, 0 otherwise
$result->requestId();           // quote this when writing to support
```

In Laravel, bind the client as a singleton in a service provider and type-hint it where you need it:

```php
$this->app->singleton(Client::class, fn () => new Client(config('services.spaw.key'), maxRetries: 2));
```

## Everything the client does

| Method | Endpoint |
| --- | --- |
| `verifyEmail($email, $callbackUrl = null, $callbackSecret = null)` | `POST /email` |
| `verifyEmails($emails)` | `POST /email/batch`, up to 50 |
| `domain($domain)` | `GET /email/domain/{domain}` |
| `createBulkJob($emails, $webhookUrl = null)`, `getBulkJob($id)`, `cancelBulkJob($id)`, `downloadBulkResults($id, $variant = null)` | the bulk endpoints, up to 100,000 addresses; the results CSV comes back as a string |
| `listSuppressions(page: 1)`, `addSuppressions($emails)`, `removeSuppression($id)` | the suppression list |
| `reportFeedback($items)`, `feedbackSummary()` | delivery outcomes and measured accuracy |
| `account()` | the live credit balance |
| `validatePhone($phone)`, `lookupIp($ip)` | the phone and IP endpoints |

A batch keeps the API's shape: `data['results']` holds one `{data, meta}` per address in input order, and the top-level `meta['stopped_reason']` is set instead of an exception when the balance runs out part-way, so paid results are never lost.

## Handle errors

Every failure throws a `Spaw\Exceptions\SpawException` or a subclass carrying `errorCode()`, `httpStatus()`, `requestId()` and, for validation failures, `errors()` keyed by field. No credits are spent on a failed request.

```php
use Spaw\Exceptions\InsufficientCreditsException;
use Spaw\Exceptions\RateLimitedException;
use Spaw\Exceptions\SpawException;

try {
    $result = $spaw->verifyEmail($email);
} catch (InsufficientCreditsException $e) {
    // nothing ran and nothing was billed
} catch (RateLimitedException $e) {
    sleep($e->retryAfter() ?? 1);
} catch (SpawException $e) {
    log($e->errorCode(), $e->getMessage(), $e->requestId());
}
```

`AuthenticationException` maps `UNAUTHENTICATED`, `ValidationException` maps `VALIDATION_FAILED`, `NotFoundException` covers a job or suppression entry that is not on the account, and `Spaw\Exceptions\ConnectionException` means no HTTP answer arrived at all. Pass `maxRetries` to the constructor to retry `429` and `5xx` answers automatically, waiting for `Retry-After` when the server sends one.

## What to do with the verdict

Branch on `deliverable`. Keep `deliverable` addresses, drop `undeliverable` ones, and route `risky` ones by `reason`: refuse `disposable` and `likely_typo`, decide `role` per use case, and treat `catch_all` as an unconfirmed rather than a bad address, using `mailbox_confidence` as the threshold. When `did_you_mean` is set, show it to the user.

## Cost

One credit per fresh deliverable or risky verdict. Undeliverable verdicts, invalid input, repeats within seven days and the `spaw.test` test addresses cost nothing, and every account gets 10 free lookups a month. Use `deliverable@spaw.test` and its siblings in your tests, or implement `Spaw\Http\Transport` to fake the API entirely.

Reference: https://spaw.co/integrations/php-sdk
