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
- Create a secret key on the API keys page and put it in your environment.
- Install the package once it is published:
composer require spaw/spaw-php
- Verify an address:
use Spaw\Client;
$spaw = new Client(getenv('SPAW_API_KEY'));
$result = $spaw->verifyEmail('[email protected]');
$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:
$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.
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 [email protected] and its siblings in your tests, or implement Spaw\Http\Transport to fake the API entirely.