Production-ready code examples for integrating BounceZero email verification into JavaScript and Node.js applications - single address lookup, async batch with concurrency control, Express.js middleware, React signup handler, CSV streaming enrichment, and webhook processing.
Node.js 18+ includes native fetch. No dependency needed.
const API_KEY = process.env.BOUNCEZERO_API_KEY;
const BASE_URL = "https://api.bouncezero.io/v1";
async function verifyEmail(email) {
const res = await fetch(`${BASE_URL}/verify`, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
});
if (!res.ok) {
throw new Error(`BounceZero API error: ${res.status}`);
}
return res.json();
}
// Usage
const result = await verifyEmail("[email protected]");
console.log(result);
// {
// email: "[email protected]",
// result: "valid", // "valid" | "invalid" | "unknown" | "catch-all"
// score: 0.97, // 0.0-1.0 deliverability score
// mx_valid: true,
// is_disposable: false,
// is_role_address: false,
// provider: "Google Workspace"
// }
⚠ Never call BounceZero directly from browser JavaScript.
Your API key embedded in frontend code is extractable from browser DevTools. Always proxy through a backend endpoint you control.
// Frontend: call YOUR backend endpoint, not BounceZero directly
async function validateEmailOnServer(email) {
const res = await fetch("/api/validate-email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
return res.json();
}
// Your backend (Express) proxies to BounceZero:
// POST /api/validate-email > BounceZero > return { valid, disposable }
const API_KEY = process.env.BOUNCEZERO_API_KEY;
async function validateEmail(req, res, next) {
const { email } = req.body;
if (!email) {
return res.status(400).json({ error: "Email is required." });
}
try {
const apiRes = await fetch("https://api.bouncezero.io/v1/verify", {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
});
const data = await apiRes.json();
if (data.result === "invalid") {
return res.status(422).json({ error: "That email address is not valid." });
}
if (data.is_disposable) {
return res.status(422).json({ error: "Please use a work or personal email address." });
}
req.emailVerification = data; // attach for downstream use
next();
} catch (err) {
// On API failure, fail open - do not block signup
console.error("BounceZero error:", err.message);
next();
}
}
module.exports = { validateEmail };
// Usage in router:
// app.post("/signup", validateEmail, signupHandler);
Use p-limit to process arrays of emails in parallel without hammering rate limits.
import pLimit from "p-limit";
const API_KEY = process.env.BOUNCEZERO_API_KEY;
const limit = pLimit(10); // max 10 concurrent requests
async function verifySingle(email) {
const res = await fetch("https://api.bouncezero.io/v1/verify", {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
});
return { email, ...(await res.json()) };
}
async function verifyBatch(emails) {
const tasks = emails.map((email) => limit(() => verifySingle(email)));
return Promise.all(tasks);
}
// Usage
const emails = ["[email protected]", "[email protected]", /* ... up to 10k */];
const results = await verifyBatch(emails);
const valid = results.filter((r) => r.result === "valid");
const invalid = results.filter((r) => r.result === "invalid");
const catchAll = results.filter((r) => r.result === "catch-all");
console.log(`Valid: ${valid.length}, Invalid: ${invalid.length}, Catch-all: ${catchAll.length}`);
import { useState, useCallback } from "react";
import { useDebounce } from "use-debounce";
export function SignupForm() {
const [email, setEmail] = useState("");
const [emailError, setEmailError] = useState(null);
const [checking, setChecking] = useState(false);
const [debouncedEmail] = useDebounce(email, 600);
const checkEmail = useCallback(async (address) => {
if (!address || !address.includes("@")) return;
setChecking(true);
try {
// Call YOUR backend, not BounceZero directly
const res = await fetch("/api/validate-email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: address }),
});
const data = await res.json();
if (data.result === "invalid") {
setEmailError("This email address doesn't look valid.");
} else if (data.is_disposable) {
setEmailError("Please use a work or personal email.");
} else {
setEmailError(null);
}
} catch {
setEmailError(null); // fail open
} finally {
setChecking(false);
}
}, []);
// Trigger on debounced change
useState(() => { checkEmail(debouncedEmail); }, [debouncedEmail]);
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="[email protected]"
/>
{checking && <span>Checking...</span>}
{emailError && <span className="error">{emailError}</span>}
<button type="submit" disabled={!!emailError || checking}>
Create Account
</button>
</form>
);
}
Process large CSV files row-by-row without loading the full file into memory.
import { createReadStream, createWriteStream } from "fs";
import { parse } from "csv-parse";
import { stringify } from "csv-stringify";
import pLimit from "p-limit";
const API_KEY = process.env.BOUNCEZERO_API_KEY;
const limit = pLimit(10);
const parser = createReadStream("input.csv").pipe(parse({ columns: true }));
const stringifier = stringify({ header: true });
stringifier.pipe(createWriteStream("output.csv"));
for await (const row of parser) {
const result = await limit(() =>
fetch("https://api.bouncezero.io/v1/verify", {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email: row.email }),
}).then((r) => r.json())
);
stringifier.write({
...row,
bz_result: result.result ?? "error",
bz_score: result.score ?? "",
bz_disposable: result.is_disposable ? "true" : "false",
bz_mx_valid: result.mx_valid ? "true" : "false",
});
}
stringifier.end();
console.log("Done - results in output.csv");
| Field | Type | Values | Use for |
|---|---|---|---|
| result | string | valid / invalid / unknown / catch-all | Primary routing decision - suppress invalid |
| score | number | 0.0 - 1.0 | Threshold-based filtering: score > 0.7 = safe to send |
| is_disposable | boolean | true / false | Block throwaway addresses at signup |
| is_role_address | boolean | true / false | Flag info@ / admin@ / sales@ for suppression in personal campaigns |
| mx_valid | boolean | true / false | Domain has mail server - basic domain health check |
| provider | string | “Gmail” / “Outlook” / etc. | Personalise outreach or route to provider-specific cadence |
New to verification? Start with the complete email verification guide.
Use the Fetch API or axios to POST to https://api.bouncezero.io/v1/verify with your API key in the Authorization header. In the browser, always call through a backend proxy - never embed your API key in client-side code where it’s extractable from DevTools.
Use p-limit to cap concurrent requests: const limit = pLimit(10); then Promise.all(emails.map(e => limit(() => verifyEmail(e)))). This runs 10 in parallel at any time. For very large lists (50k+), use the BounceZero bulk upload endpoint instead of individual API calls.
Always on the server. Your API key embedded in frontend JavaScript is extractable from browser DevTools. Set up a POST /api/validate-email endpoint in Express (or Next.js API routes) that proxies to BounceZero and returns only the fields your frontend needs.
100 free credits on signup. No credit card. Instant access to the verification API. $3/1K thereafter - credits never expire.
Ayoub built BounceZero's 5-stage validation pipeline, its dedicated BGP-announced IP infrastructure, and the Patroni HA PostgreSQL cluster behind every verification. Previously built high-volume email delivery infrastructure. Trained at 1337 Benguerir (École 42 network, 2019). Open-source: bgp_analyzer.
Deep-dive guides on how email verification and inbox placement work
272,446-domain census: DMARC gap, provider divide, catch-all rates
10.2M verifications: 12.3% of addresses are dead, and where they hide
826K re-verifications: only 19% of valid addresses survive 90 days
True catch-all is 1.4% - most of what looks catch-all is unprobeable providers
info@ bounces 4.5x more than personal addresses - measured, not guessed
The 3x invalid-rate gap that vanishes when you control for domain size
Explore other topics