The official Node.js client for the BounceZero email verification API. Zero dependencies, Node 18+, with TypeScript definitions included and a small, predictable, Promise-based surface built for production — from a signup form that validates one address in real time to a nightly job that cleans millions.
const BounceZero = require("bouncezero");
const client = new BounceZero("bz_live_...");
const result = await client.verify("someone@example.com");
console.log(result.classification, result.score); // → verified 98Every address is scored across 40+ signals through a five-stage pipeline — syntax, DNS/MX, SMTP mailbox probing, disposable & role detection, and a Bayesian + ML confidence model — so you catch invalid, risky, and low-value addresses before they cost you deliverability.
- ⚡ Real-time single verification in ~500 ms–2 s, or async bulk for large lists
- 🎯 Six clear verdicts (
verified/invalid/catch_all/disposable/risky/unknown) plus threat flags - 🔁 Idempotent bulk submits — a network retry can never double-charge you
- 🧪 Sandbox keys to build & test without spending credits
- 🔒 Signed webhooks with a one-line verifier
- 📦 Zero dependencies, typed errors, automatic retries with backoff, first-class TypeScript
npm install bouncezeroRequires Node.js 18+ (uses the built-in fetch). No transitive dependencies.
Grab your API key from the dashboard and pass it to the client. Keep live keys server-side — never ship them in client code.
const BounceZero = require("bouncezero");
// or: import BounceZero from "bouncezero";
const client = new BounceZero("bz_live_...");// 1 — Verify a single address
const result = await client.verify("jane@example.com");
if (result.classification === "verified") addToMailingList(result.email);
// 2 — Batch up to 100 synchronously
const report = await client.verifyBatch(["a@example.com", "b@example.com"]);
console.log(report.summary); // { verified: 1, invalid: 1, ... }
// 3 — Async bulk for large lists (idempotency-safe)
const job = await client.verifyBulk(emails, { idempotencyKey: "import-2026-07-11" });
const final = await client.waitForBulk(job.job_id);
const csv = await client.bulkDownload(job.job_id);verify() resolves to an object with the full signal breakdown. The fields you'll use most:
| Field | Type | Description |
|---|---|---|
email |
string |
The address that was checked |
classification |
string |
One of the six verdicts below |
score |
number |
Confidence 0–100 |
is_deliverable |
boolean | null |
true/false, or null when inconclusive |
risk_level |
string |
low · medium · high |
recommendation |
string |
Human-readable guidance |
| Verdict | Meaning | Safe to send? |
|---|---|---|
verified |
Mailbox exists and accepts mail | ✅ |
invalid |
Mailbox does not exist — will hard bounce | ❌ |
catch_all |
Domain accepts everything; mailbox unconfirmable | |
disposable |
Temporary/throwaway provider | |
risky |
Concrete negative signals (full mailbox, poor reputation…) | |
unknown |
Mail server blocked/greylisted the probe — retry later | ⏳ |
Two rare threat verdicts — complainer and spamtrap — may also appear; remove those addresses immediately (details in threat_type).
| Method | Description |
|---|---|
verify(email, depth = "standard") |
Verify one address. depth: standard · deep · ultra |
verifyBatch(emails) |
Verify up to 100 addresses synchronously |
verifyBulk(emails, { idempotencyKey }) |
Submit an async bulk job |
bulkStatus(jobId) |
Poll job progress |
bulkResults(jobId, { limit, offset }) |
Paginated results |
bulkDownload(jobId) |
Download completed results as a CSV string |
waitForBulk(jobId, { pollInterval, timeout }) |
Await until a job finishes |
domain(domain) |
Domain-level intelligence (MX, provider, catch-all…) |
analyzeList(emails) |
Free list-quality analysis — no verification, no credits |
BounceZero.verifyWebhookSignature(body, header, secret) |
Validate a webhook signature |
Bulk jobs run asynchronously. Submit, then either await or poll:
const job = await client.verifyBulk(emails, { idempotencyKey: "crm-sync-42" });
// Option A — await until done
const final = await client.waitForBulk(job.job_id, { pollInterval: 10000 });
// Option B — poll yourself
const status = await client.bulkStatus(job.job_id);
console.log(`${status.completed} / ${status.total}`);
// Fetch results (paginated) or download the full CSV
const page = await client.bulkResults(job.job_id, { limit: 1000, offset: 0 });
const csv = await client.bulkDownload(job.job_id);Idempotency — pass an idempotencyKey (any unique string). Replaying the same key returns the original job instead of creating and charging for a duplicate, so retries after a timeout are always safe.
Every failure throws a subclass of BounceZeroError, each carrying .statusCode and .payload:
const {
BounceZero, AuthenticationError, InsufficientCreditsError,
RateLimitError, NotFoundError, BounceZeroError,
} = require("bouncezero");
try {
const result = await client.verify("someone@example.com");
} catch (e) {
if (e instanceof InsufficientCreditsError) topUpCredits();
else if (e instanceof RateLimitError) await sleep((e.retryAfter || 60) * 1000);
else if (e instanceof AuthenticationError) alert("Check your API key");
else throw e;
}| Error | HTTP | When |
|---|---|---|
AuthenticationError |
401 / 403 | Missing, invalid, disabled, or IP-restricted key |
InsufficientCreditsError |
402 | Not enough credits |
NotFoundError |
404 | Unknown job or resource |
RateLimitError |
429 | Plan rate limit exceeded (see .retryAfter) |
BounceZeroError |
other | Any other API/transport error |
Requests automatically retry on 429 and 5xx with exponential backoff (honoring Retry-After); tune with maxRetries.
Limits are per API key, per minute, by plan. Responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset.
| Plan | Requests / min |
|---|---|
| Free | 30 |
| Starter · Growth | 100 |
| Professional · Business · Scale | 300 |
| Ultimate · Enterprise | 1000 |
Create a sandbox key (bz_test_ prefix) in the dashboard. It returns deterministic mock results, never spends credits, and never performs a real probe — the local part of the address picks the verdict:
const sandbox = new BounceZero("bz_test_...");
(await sandbox.verify("verified@example.com")).classification; // → "verified"
(await sandbox.verify("risky@example.com")).classification; // → "risky"Supported on verify(). See the sandbox docs.
BounceZero signs every webhook with X-BounceZero-Signature (HMAC-SHA256). Always verify against the raw request body before parsing:
const BounceZero = require("bouncezero");
app.post("/webhooks/bouncezero", express.raw({ type: "application/json" }), (req, res) => {
const ok = BounceZero.verifyWebhookSignature(
req.body, // raw Buffer
req.headers["x-bouncezero-signature"],
signingSecret, // from Dashboard → Webhooks
);
if (!ok) return res.status(400).end();
const event = JSON.parse(req.body);
// ...
res.status(200).end();
});const client = new BounceZero("bz_live_...", {
baseUrl: "https://app.bouncezero.io", // override for a dedicated region
timeout: 60000, // milliseconds
maxRetries: 2, // retries on 429 / 5xx
});Types ship with the package — no @types install needed.
import BounceZero, { VerifyResult, RateLimitError } from "bouncezero";
const client = new BounceZero(process.env.BOUNCEZERO_API_KEY!);
const result: VerifyResult = await client.verify("someone@example.com");- 📖 Documentation — https://docs.bouncezero.io
- 📊 API status — https://status.bouncezero.io
- ✉️ support@bouncezero.io
MIT © BounceZero Ltd