The Node.js client is a thin wrapper over the REST API: one method per endpoint, the API's data and meta blocks returned exactly as documented, TypeScript declarations, and an error class for every code. It has no dependencies, because it uses the fetch built into Node.js 18 and newer, and it is an ES module.
The package is built and will be published on npm as spaw. Until the listing is live, a plain fetch against the API reference does the same job. For code that runs in a browser, use the form helper with a publishable key instead; the secret key this client needs must never ship to a page.
Install and verify one address
- Create a secret key on the API keys page and put it in an environment variable.
- Install the package once it is published:
npm install spaw
- Verify an address:
import Spaw from 'spaw';
const spaw = new Spaw({ apiKey: process.env.SPAW_API_KEY });
const result = await spaw.verifyEmail('[email protected]');
console.log(result.data.deliverable); // "deliverable", "risky" or "undeliverable"
console.log(result.data.reason); // e.g. "catch_all", or null when deliverable
console.log(result.data.risk_score); // 0–100, a sum of published weights
console.log(result.meta.credits_used); // 1 for a fresh answered lookup, 0 otherwise
result.data carries all 27 documented fields, including did_you_mean, mx_provider and smtp_checked_at, and every reason value is explained on the verdict reasons pages.
Everything the client does
| Method | Endpoint |
|---|---|
verifyEmail(email, options) |
POST /email, with callbackUrl and callbackSecret for async settling |
verifyEmails(emails) |
POST /email/batch, up to 50 |
domain(domain) |
GET /email/domain/{domain} |
createBulkJob(emails, { webhookUrl }), getBulkJob(id), cancelBulkJob(id), downloadBulkResults(id, { variant }) |
the bulk endpoints, up to 100,000 addresses |
listSuppressions({ page }), addSuppressions(emails), removeSuppression(id) |
the suppression list |
reportFeedback(items), feedbackSummary() |
delivery outcomes and measured accuracy |
account() |
the live credit balance |
validatePhone(phone, { country }), 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 meta.stopped_reason is set instead of throwing when the balance runs out part-way, so paid results are never lost. A bulk job returns its webhook_secret once; keep it, because it signs the completion webhook.
Handle errors
Every failure throws a subclass of SpawError carrying code, message, httpStatus and requestId; no credits are spent on a failed request.
import Spaw, { InsufficientCreditsError, RateLimitedError, SpawError } from 'spaw';
try {
const result = await spaw.verifyEmail('[email protected]');
} catch (error) {
if (error instanceof InsufficientCreditsError) {
// nothing ran and nothing was billed
} else if (error instanceof RateLimitedError) {
// error.retryAfter seconds, from the Retry-After header
} else if (error instanceof SpawError) {
console.error(error.code, error.httpStatus, error.requestId);
}
}
AuthenticationError maps UNAUTHENTICATED, ValidationError maps VALIDATION_FAILED with errors keyed by field, NotFoundError covers a bulk job or suppression entry that is not on the account, and SpawConnectionError means the API could not be reached. Pass maxRetries: 2 to the constructor to retry 429 and 5xx answers automatically, honouring Retry-After.
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 address whose mailbox cannot be confirmed rather than a bad one, using mailbox_confidence as the threshold. When did_you_mean is set, offer it back 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. Point your test suite at [email protected] and its siblings: fixed answers, no credits, never logged.