# Spaw Node.js SDK: verify emails from JavaScript

The official spaw package for Node.js 18 and newer wraps every Spaw endpoint with typed results, TypeScript declarations and one error class per API code.

Updated: 2026-09-03

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

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

```bash
npm install spaw
```

3. Verify an address:

```js
import Spaw from 'spaw';

const spaw = new Spaw({ apiKey: process.env.SPAW_API_KEY });

const result = await spaw.verifyEmail('mia@acme.com');
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.

```js
import Spaw, { InsufficientCreditsError, RateLimitedError, SpawError } from 'spaw';

try {
    const result = await spaw.verifyEmail('mia@acme.com');
} 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 `deliverable@spaw.test` and its siblings: fixed answers, no credits, never logged.

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