diff --git a/.github/workflows/sdk_ci.yml b/.github/workflows/sdk_ci.yml
index c507f66..63402c8 100644
--- a/.github/workflows/sdk_ci.yml
+++ b/.github/workflows/sdk_ci.yml
@@ -28,5 +28,11 @@ jobs:
- name: Install dependencies
run: npm ci
+ - name: Run tests and typecheck
+ run: npm test
+
- name: Run tests with coverage
run: npm run test:coverage
+
+ - name: Typecheck declarations
+ run: npm run dts-check
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..3a333d6
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,64 @@
+# AGENTS.md
+
+Guidance for AI coding agents working in this repository.
+
+## Project
+
+`@zerobounce/zero-bounce-sdk` is the ZeroBounce JavaScript SDK. Callers construct `ZeroBounceSDK`, call `init` with an API key, and `await` the method. HTTP goes through global `fetch` inside `createRequest` in `src/utils.js`. Releases are git tags. The Publish workflow ships that tag to npm.
+
+## Layout
+
+- `src/zero-bounce.js`: public client.
+- `src/utils.js`: URLs, timeouts, and HTTP errors.
+- `src/index.js`: default export used by the webpack build.
+- `zeroBounceSDK.d.ts`: published TypeScript types. `npm run dts-check` typechecks this file. `tests/ts-consumer.ts` typechecks a consumer (`npm run test:types`).
+- `tests/offline-api.test.js`: offline suite. A fake `fetch` records every request and answers without the network.
+- `tests/zero-bounce.test.js` and `tests/getfile-node.test.js`: existing Jest cases.
+- `package.json` and `package-lock.json`: the version fields. There is no runtime version constant and no custom User-Agent.
+
+## Commands
+
+```bash
+npm ci
+npm test # jest && tsc --noEmit -p tsconfig.test.json
+npm run dts-check # tsc --noEmit on zeroBounceSDK.d.ts
+npm run test:coverage # jest --coverage
+npm run build # webpack production bundle; also runs from prepublishOnly
+```
+
+There is no separate lint command. CI (`.github/workflows/sdk_ci.yml`) runs `npm test`, coverage, and `npm run dts-check`. The publish workflow's test gate is `npm test`, which is stricter than coverage alone because it includes the TypeScript consumer check.
+
+## Conventions
+
+- **One `ZeroBounceSDK` per API key.** The key is stored on the instance. Do not reuse one instance across users or requests. Construct a separate object per key. The offline concurrency test does this.
+- **Column indexes are 1-based and are not converted.** Optional name, gender, and IP columns use `0` or `false` to mean "leave this field out". Required `email_address_column` and `domain_column` are sent as given, including `0`. `0` is not "missing".
+- **Optional parameters are omitted when unset.** `timeout`, `activityData`, `verifyPlus`, `allowPhase2`, `downloadType`, `ip_address`, and email-finder name fields are not sent until the caller sets them. Explicit `false` is sent as the string `false` on query, form, and multipart bodies, and as a JSON boolean on `validateBatch`.
+- **Do not use boolean `false` as the "omit this column" sentinel inside `_sendBulkRootMultipartFile`.** `false` is a real `has_header_row` value and must be appended as `"false"`. Optional columns that are unset are `null`, and `null` is omitted. `optionalColumn` turns `0` and `false` into `null` for those optional columns only.
+- **`allow_phase_2` is validation bulk only** (`sendFile`, `sendFileStream`, `validateS3file`), and only when `allowPhase2` is set. Scoring `sendFile` ignores it. `activity_data` on get-file is validation `getFile` only.
+- **`validateBatch` uses `api.zerobounce.net`**, not `bulkapi.zerobounce.net`. Bulk email finder and domain search use the bulk host root (`/email-finder/…`, `/domain-search/…`) with no `/v2`. List evaluator and validation/scoring file methods use `https://bulkapi.zerobounce.net/v2`.
+- **Failures reject the promise.** The message must be short: no raw HTML, no megabyte body, no `SyntaxError` / `Unexpected token` text, and no `Error: Error:` or `[Error]:` prefix. An account-level `validateBatch` error (`email_address` `all`) rejects. A per-address error stays on the resolved result. A top-level `error` string for an invalid API key or no credits rejects. `{ error: "invalid email" }` from `scoreEmail` stays on the result. A bulk JSON body such as `{ success: "False", message: […] }` is returned when it is not one of those account-level `error` strings.
+- **`getFile` in a browser** downloads the CSV and resolves to the filename. **In Node** it resolves to the CSV text. A JSON error body (for example `success: false`) is returned as the parsed object. HTML, an empty body, and HTTP status 400 or higher reject.
+- **`init` requires an `https://` base URL** and a non-blank API key. Requests time out after 120 seconds. `fetch` is called with `redirect: "error"`.
+- **Tests replace global `fetch`.** Do not call the live API from `npm test`. `tests/real-api-prod.mjs` is opt-in (`npm run test:real-api`) and is not part of `npm test`.
+- Match the surrounding style. Do not reformat unrelated code. Do not commit `dist/`, `node_modules/`, `coverage/`, or `.env`.
+
+## Changelog
+
+`CHANGELOG.md` follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html).
+
+- Add every user-visible change to `## [Unreleased]` in the same commit as the change.
+- Group entries under these headings, in this order, omitting empty ones: `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`.
+- Write for SDK users: name the method, option, or behaviour, and say what changed. Leave out CI-only changes, internal refactors, and fixes to code that was never released.
+- At 2.y.z, a breaking change bumps the major version, a new method or option bumps the minor version, and fixes alone bump the patch version.
+
+## Releasing
+
+Publishing and pushing tags need a maintainer's explicit approval. Do not bump the version, tag, or publish unless they asked.
+
+1. Move `[Unreleased]` entries under `## [x.y.z] - YYYY-MM-DD`, leave an empty `[Unreleased]` section, and update the compare links at the bottom of `CHANGELOG.md`.
+2. Set the same version in all of these. The publish workflow fails if `package.json` differs from the tag:
+ - `package.json` `"version"`
+ - `package-lock.json` top-level `"version"`
+ - `package-lock.json` `packages[""].version`
+3. `npm test`, `npm run dts-check`, and `npm run test:coverage`.
+4. Tag `vx.y.z` on that commit and run the Publish workflow with that tag. It publishes `@zerobounce/zero-bounce-sdk` to npm and creates the GitHub release. There is no runtime version constant to update.
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..17a25b8
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,101 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+### Added
+
+- `validateEmail` accepts `activityData`, `verifyPlus`, and `method: "POST"` (form-encoded `POST /v2/validate`). `timeout` is still optional and must be from 3 to 60 seconds.
+- `validateBatch` accepts `timeout` (10–120 seconds), `activityData`, and `verifyPlus`.
+- `scoreEmail` calls `GET /v2/scoring`. `scoreEmail(email, { method: "POST" })` sends a form-encoded `POST /v2/scoring`.
+- Bulk email finder: `sendEmailFinderFile`, `sendEmailFinderFileStream`, `getEmailFinderFileStatus`, `getEmailFinderFile`, `deleteEmailFinderFile`.
+- Bulk domain search: `sendDomainSearchFile`, `sendDomainSearchFileStream`, `getDomainSearchFileStatus`, `getDomainSearchFile`, `deleteDomainSearchFile`.
+- List evaluator: `sendListEvaluatorFile`, `sendListEvaluatorFileStream`, `getListEvaluatorResult`, `sendEvaluatedFile`.
+- Filters: `addFilter`, `listFilters` (GET, or POST when `method` is `"POST"`), `deleteFilter`.
+- S3 bulk: `validateS3file`, `scoreS3file`, `exportS3file`, `listS3files`.
+
+### Changed
+
+- `init` rejects an `apiBaseURL` that is not `https://`.
+- API requests abort after 120 seconds and do not follow redirects.
+- Missing or invalid arguments reject the promise. They no longer log with `console.error` and return `undefined`.
+
+### Fixed
+
+- A 403 HTML "Access denied" page is a short error. `getFile` no longer puts the page in the error (about 1.4 MB from the live API). An HTML page with status 200 is an error as well, including `getFile`, `getEmailFinderFile`, and `getDomainSearchFile`, instead of a saved CSV or a JSON parse.
+- Empty and non-JSON bodies are a short error. The message is no longer `SyntaxError: …` text, and a repeated `Error:` / `[Error]:` prefix is removed.
+- `validateBatch` rejects an account-level failure (HTTP 200, error for address `all`, such as an invalid API key or no credits). An error for one address stays on the resolved result. A top-level `error` string for an invalid API key or no credits also rejects. A per-email scoring error such as `{ error: "invalid email" }` stays on the resolved result.
+- `sendEmailFinderFile` and `sendDomainSearchFile` send `has_header_row` when it is set to false. `sendFile` and `sendScoringFile` send `has_header_row` and `remove_duplicate` as `true` or `false`.
+- Required column indexes are sent as given, including `0`: `email_address_column` on `sendFile`, `sendFileStream`, `sendScoringFile`, `sendScoringFileStream`, and the list evaluator, and `domain_column` on bulk email finder and domain search. Optional name, gender, and IP columns of `0` are still left out.
+- `findEmailByDomain`, `findEmailByCompanyName`, and `guessFormat` omit unset name fields. They no longer send the string `null`.
+- `init("")` and a whitespace-only API key throw before any request.
+
+### Security
+
+- Redirects are disabled, so a response cannot send the client, or the API key in the query string, to another host.
+- A custom API base URL cannot be `http://`.
+- Rejected errors no longer include the raw HTML access-denied page.
+
+## [2.1.8] - 2026-06-18
+
+### Fixed
+
+- `validateBatch` posts to `api.zerobounce.net/v2/validatebatch` instead of `bulkapi.zerobounce.net`.
+
+## [2.1.6] - 2026-06-11
+
+### Fixed
+
+- `getFile` and `getScoringFile` return the CSV text in Node.js. In a browser they still download `result.csv` or `result-scoring.csv`.
+
+## [2.1.5] - 2026-06-10
+
+### Added
+
+- Validate responses include `catchall_domain`.
+
+## [2.1.0] - 2026-04-05
+
+### Added
+
+- `sendFile` and `sendFileStream` take `allowPhase2`, sent as `allow_phase_2` on validation bulk only.
+- `getFile` and `getScoringFile` take `downloadType` (`ZeroBounceSDK.ZBDownloadType`: `phase_1`, `phase_2`, `combined`). Validation `getFile` also takes `activityData`.
+- TypeScript response types for validation, batch validation, and API usage.
+
+## [2.0.11] - 2026-03-03
+
+### Changed
+
+- License the project under MIT.
+
+## [2.0.0] - 2026-03-03
+
+### Added
+
+- TypeScript declarations in `zeroBounceSDK.d.ts`.
+- `package.json` `exports` for Node.js.
+
+## [1.3.0] - 2026-01-15
+
+### Added
+
+- First tagged release: `getCredits`, `validateEmail` (including `timeout`), `getApiUsage`, `validateBatch`, `getEmailActivity`, bulk validation and scoring file methods, `findEmailByDomain`, `findEmailByCompanyName`, `findEmailFormatByDomain`, and `findEmailFormatByCompanyName`.
+- `init(apiKey, apiBaseURL)` selects the default, US, or EU API host.
+- `ZBValidateSubStatus.ACCEPT_ALL`.
+
+### Deprecated
+
+- `guessFormat`. Use the email-finder or domain-search methods instead.
+
+[Unreleased]: https://github.com/zerobounce/zero-bounce-javascript/compare/v2.1.11...HEAD
+[2.1.8]: https://github.com/zerobounce/zero-bounce-javascript/compare/v2.1.6...v2.1.8
+[2.1.6]: https://github.com/zerobounce/zero-bounce-javascript/compare/v2.1.5...v2.1.6
+[2.1.5]: https://github.com/zerobounce/zero-bounce-javascript/compare/v2.1.0...v2.1.5
+[2.1.0]: https://github.com/zerobounce/zero-bounce-javascript/compare/v2.0.11...v2.1.0
+[2.0.11]: https://github.com/zerobounce/zero-bounce-javascript/compare/v2.0.0...v2.0.11
+[2.0.0]: https://github.com/zerobounce/zero-bounce-javascript/compare/v1.3.0...v2.0.0
+[1.3.0]: https://github.com/zerobounce/zero-bounce-javascript/releases/tag/v1.3.0
diff --git a/src/utils.js b/src/utils.js
index ea3e60c..acd0947 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -1,5 +1,6 @@
// Constants
export const API_BULK_BASE_URL = "https://bulkapi.zerobounce.net/v2";
+export const API_BULK_ROOT_URL = "https://bulkapi.zerobounce.net";
export const HTTP_TIMEOUT_MS = 120000;
export const HEADERS = {
Accept: "*/*",
@@ -9,7 +10,6 @@ export const HEADERS = {
export function requireHttpsUrl(url) {
if (typeof url !== "string" || !/^https:\/\//i.test(url)) {
- parameterIsInvalid("apiBaseURL", "Must be an https:// URL.");
throw new Error("apiBaseURL must be an https:// URL");
}
return url;
@@ -64,8 +64,139 @@ function isBrowserEnvironment() {
);
}
+function responseStatus(response) {
+ return response && typeof response.status === "number" ? response.status : 200;
+}
+
+function responseContentType(response) {
+ if (!response || !response.headers || typeof response.headers.get !== "function") return "";
+ return String(response.headers.get("content-type") || "").toLowerCase();
+}
+
+function looksLikeHtml(body) {
+ const head = String(body).slice(0, 2000).toLowerCase();
+ return (
+ head.includes("]*>([\s\S]*?)<\/title>/i);
+ if (!match) return "";
+ return match[1].replace(/\s+/g, " ").trim();
+}
+
+function stripErrorPrefix(message) {
+ let value = String(message).trim();
+ for (;;) {
+ if (value.startsWith("Error:")) {
+ value = value.slice("Error:".length).trim();
+ continue;
+ }
+ if (value.startsWith("[Error]:")) {
+ value = value.slice("[Error]:".length).trim();
+ continue;
+ }
+ return value;
+ }
+}
+
+function isAccountLevelText(value) {
+ return /invalid api key|api_key is invalid|ran out of credits|out of credits/i.test(value);
+}
+
+function readableJsonMessage(parsed) {
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
+ if (typeof parsed.error === "string" && parsed.error.trim()) return parsed.error.trim();
+ if (typeof parsed.message === "string" && parsed.message.trim()) return parsed.message.trim();
+ if (Array.isArray(parsed.message)) {
+ const parts = parsed.message.filter((item) => typeof item === "string" && item.trim());
+ if (parts.length) return parts.join("; ");
+ }
+ return null;
+}
+
+function accountLevelMessage(parsed) {
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
+ if (Array.isArray(parsed.errors)) {
+ for (const item of parsed.errors) {
+ if (item && String(item.email_address || "").toLowerCase() === "all") {
+ const text = typeof item.error === "string" ? item.error.trim() : "";
+ return stripErrorPrefix(text || "Invalid API Key or your account ran out of credits");
+ }
+ }
+ }
+ if (typeof parsed.error === "string" && isAccountLevelText(parsed.error)) {
+ return stripErrorPrefix(parsed.error.trim());
+ }
+ return null;
+}
+
+function cloudflareTitle(parsed) {
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || typeof parsed.title !== "string") {
+ return null;
+ }
+ const accessDenied = parsed.title.includes("Access denied") || parsed.title.includes("1020");
+ if (accessDenied && parsed.cloudflare_error === true) return parsed.title;
+ return null;
+}
+
+function messageFromBody(body, statusCode) {
+ const trimmed = String(body).trim();
+ if (!trimmed) return `Empty response from ZeroBounce (HTTP ${statusCode})`;
+ if (looksLikeHtml(body)) {
+ const title = htmlTitle(body);
+ if (!title) return "ZeroBounce returned an HTML error page instead of an API response.";
+ return `ZeroBounce returned an HTML error page ("${title}") instead of an API response.`;
+ }
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
+ try {
+ const parsed = JSON.parse(trimmed);
+ const title = cloudflareTitle(parsed);
+ if (title) return title;
+ const account = accountLevelMessage(parsed);
+ if (account) return account;
+ const readable = readableJsonMessage(parsed);
+ if (readable) return stripErrorPrefix(readable);
+ return "ZeroBounce returned an unexpected response.";
+ } catch {
+ return "ZeroBounce returned a response that is not valid JSON.";
+ }
+ }
+ if (trimmed.length > 1000) return "ZeroBounce returned a response that is not valid JSON.";
+ const message = stripErrorPrefix(trimmed);
+ return message || `Empty response from ZeroBounce (HTTP ${statusCode})`;
+}
+
+function transportError(error) {
+ const name = error && error.name ? error.name : "Error";
+ const message = error && error.message ? error.message : String(error);
+ const redacted = `${name}: ${message}`.replace(/([?&]api_key=)[^&\s"']+/gi, "$1***");
+ return new Error(redacted);
+}
+
+async function readBodyText(response) {
+ if (response && typeof response.text === "function") return await response.text();
+ if (response && typeof response.json === "function") {
+ const data = await response.json();
+ return typeof data === "string" ? data : JSON.stringify(data);
+ }
+ return "";
+}
+
function processGetFileResponseBody(finalResult, response, scoring) {
- const ct = (response.headers.get("content-type") || "").toLowerCase();
+ const status = responseStatus(response);
+ const ct = responseContentType(response);
+ if (looksLikeHtml(finalResult) || ct.includes("text/html") || status >= 400) {
+ throw new Error(messageFromBody(finalResult, status));
+ }
+ if (!String(finalResult).trim()) {
+ throw new Error(`Empty response from ZeroBounce (HTTP ${status})`);
+ }
+
const trimmed = finalResult.trim();
const looksJson =
ct.includes("application/json") || trimmed.startsWith("{") || trimmed.startsWith("[");
@@ -77,15 +208,7 @@ function processGetFileResponseBody(finalResult, response, scoring) {
return parsed;
}
} catch {
- /* treat as file body */
- }
- }
-
- if (!response.ok) {
- try {
- return JSON.parse(finalResult);
- } catch {
- throw new Error(finalResult || `HTTP ${response.status}`);
+ throw new Error("ZeroBounce returned a response that is not valid JSON.");
}
}
@@ -97,41 +220,77 @@ function processGetFileResponseBody(finalResult, response, scoring) {
return saveFile(blob, `result${scoring ? "-scoring" : ""}.csv`);
}
+function bulkApiBase(bulkBase) {
+ return bulkBase === "root" ? API_BULK_ROOT_URL : API_BULK_BASE_URL;
+}
+
+function buildRequestUrl({ batch, bulkBase, apiBaseURL, path, params }) {
+ const base = batch ? bulkApiBase(bulkBase) : apiBaseURL;
+ const query =
+ params != null && Object.keys(params).length > 0
+ ? `?${new URLSearchParams(params)}`
+ : "";
+ return `${base}${path}${query}`;
+}
+
export async function createRequest({
requestType,
body = null,
params = null,
path,
batch = false,
+ bulkBase = "v2",
returnText = false,
scoring = false,
apiBaseURL
}) {
- const url = `${
- batch ? API_BULK_BASE_URL : apiBaseURL
- }${path}?${new URLSearchParams(params)}`;
+ const url = buildRequestUrl({ batch, bulkBase, apiBaseURL, path, params });
+ let response;
try {
- const response = await fetch(url, {
+ response = await fetch(url, {
method: requestType,
headers: HEADERS,
body,
signal: fetchTimeoutSignal(),
+ redirect: "error",
});
- if (returnText) {
- const finalResult = await response.text();
- return processGetFileResponseBody(finalResult, response, scoring);
- }
- if (response.status === 403) {
- throw new Error('[Error]: api_key is invalid');
- } else {
- const finalResult = await response.json();
- return finalResult;
- }
} catch (error) {
- // Some fetch polyfills (e.g. node-fetch) embed the request URL, including api_key, in errors.
- throw new Error(String(error).replace(/([?&]api_key=)[^&\s"']+/gi, "$1***"));
+ // Some fetch polyfills embed the request URL, including api_key, in errors.
+ throw transportError(error);
}
+
+ let finalResult;
+ try {
+ finalResult = await readBodyText(response);
+ } catch (error) {
+ throw transportError(error);
+ }
+
+ if (returnText) {
+ return processGetFileResponseBody(finalResult, response, scoring);
+ }
+
+ const status = responseStatus(response);
+ const contentType = responseContentType(response);
+ if (
+ looksLikeHtml(finalResult) ||
+ contentType.includes("text/html") ||
+ status >= 400 ||
+ !String(finalResult).trim()
+ ) {
+ throw new Error(messageFromBody(finalResult, status));
+ }
+
+ let parsed;
+ try {
+ parsed = JSON.parse(finalResult);
+ } catch {
+ throw new Error(messageFromBody(finalResult, status));
+ }
+ const account = accountLevelMessage(parsed);
+ if (account) throw new Error(account);
+ return parsed;
}
function saveFile(blob, filename) {
@@ -157,17 +316,15 @@ function saveFile(blob, filename) {
}
export function notInitialized() {
- console.error("ZeroBounce: Call init function first with a valid api key.");
+ throw new Error("ZeroBounce: Call init function first with a valid api key.");
}
export function parameterIsMissing(parameter, aditionalInfo = "") {
- console.error(
- `ZeroBounce: ${parameter} parameter is missing. ${aditionalInfo}`
- );
+ const extra = aditionalInfo ? ` ${aditionalInfo}` : "";
+ throw new Error(`ZeroBounce: ${parameter} parameter is missing.${extra}`);
}
export function parameterIsInvalid(parameter, aditionalInfo = "") {
- console.error(
- `ZeroBounce: ${parameter} parameter is invalid. ${aditionalInfo}`
- );
+ const extra = aditionalInfo ? ` ${aditionalInfo}` : "";
+ throw new Error(`ZeroBounce: ${parameter} parameter is invalid.${extra}`);
}
diff --git a/src/zero-bounce.js b/src/zero-bounce.js
index 786c6d4..be8e3f3 100644
--- a/src/zero-bounce.js
+++ b/src/zero-bounce.js
@@ -65,6 +65,18 @@ export const ZBDownloadType = Object.freeze({
COMBINED: "combined"
});
+function isMissing(value) {
+ return value == null || (typeof value === "string" && value.trim() === "");
+}
+
+function booleanText(value) {
+ return value ? "true" : "false";
+}
+
+function optionalColumn(value) {
+ return value ? value : null;
+}
+
export class ZeroBounceSDK {
static ApiURL = Object.freeze({
DEFAULT_API_URL: "https://api.zerobounce.net/v2",
@@ -90,16 +102,15 @@ export class ZeroBounceSDK {
* @param apiBaseURL - your preferred API, possible values being ApiURL.DEFAULT_API_URL, ApiURL.USAAPIURL, ApiURL.EUAPIURL
* */
init(apiKey, apiBaseURL = ZeroBounceSDK.ApiURL.DEFAULT_API_URL) {
- if (!apiKey) {
- parameterIsMissing("Api key", "Please provide a valid API key.");
- } else {
- this._api_key = apiKey;
- this._api_base_url = requireHttpsUrl(apiBaseURL);
- this._initialized = true;
+ if (apiKey == null || String(apiKey).trim() === "") {
+ throw new Error("ZeroBounce: Api key parameter is missing. Please provide a valid API key.");
}
+ this._api_key = apiKey;
+ this._api_base_url = requireHttpsUrl(apiBaseURL);
+ this._initialized = true;
}
- getCredits() {
+ async getCredits() {
if (!this._initialized) {
notInitialized();
return;
@@ -115,23 +126,30 @@ export class ZeroBounceSDK {
* @param options - options object or ip_address string for backwards compatibility
* @param options.ip_address - IP address (optional)
* @param options.timeout - validation timeout in seconds, 3-60 (optional). If met, the API will return unknown/greylisted.
+ * @param options.activityData - when true/false, sends activity_data query param (optional)
+ * @param options.verifyPlus - when true/false, sends verify_plus query param (optional)
+ * @param options.method - `"POST"` for form-encoded POST (default GET)
* */
- validateEmail(email, options = null) {
+ async validateEmail(email, options = null) {
if (!this._initialized) {
notInitialized();
return;
- } else if (!email) {
+ } else if (isMissing(email)) {
parameterIsMissing("Email");
return;
}
let ip_address;
let timeout;
+ let activityData;
+ let verifyPlus;
if (typeof options === "string") {
ip_address = options;
} else if (options && typeof options === "object") {
ip_address = options.ip_address;
timeout = options.timeout;
+ activityData = options.activityData;
+ verifyPlus = options.verifyPlus;
}
if (timeout != null && (timeout < 3 || timeout > 60)) {
@@ -145,6 +163,21 @@ export class ZeroBounceSDK {
};
if (ip_address != null) params.ip_address = ip_address;
if (timeout != null) params.timeout = timeout;
+ if (activityData != null) params.activity_data = activityData ? "true" : "false";
+ if (verifyPlus != null) params.verify_plus = verifyPlus ? "true" : "false";
+
+ const usePost =
+ options && typeof options === "object" && options.method === "POST";
+
+ if (usePost) {
+ return createRequest({
+ requestType: "POST",
+ path: "/validate",
+ body: new URLSearchParams(params),
+ params: {},
+ apiBaseURL: this._api_base_url,
+ });
+ }
return createRequest({ requestType: "GET", params, path: "/validate", apiBaseURL: this._api_base_url });
}
@@ -153,14 +186,14 @@ export class ZeroBounceSDK {
* @param startDate - The start date
* @param endDate - The end date
* */
- getApiUsage(startDate, endDate) {
+ async getApiUsage(startDate, endDate) {
if (!this._initialized) {
notInitialized();
return;
- } else if (!startDate) {
+ } else if (isMissing(startDate)) {
parameterIsMissing("Start date", "Format: YYYY-MM-DD");
return;
- } else if (!endDate) {
+ } else if (isMissing(endDate)) {
parameterIsMissing("End date", "Format: YYYY-MM-DD");
return;
}
@@ -180,21 +213,39 @@ export class ZeroBounceSDK {
/**
* @param emailBatch: an array containing a list of email objects {email_address: "example@example.com"}
+ * @param options - optional timeout (10-120 seconds), activityData, verifyPlus
* */
- validateBatch(emailBatch) {
+ async validateBatch(emailBatch, options = null) {
if (!this._initialized) {
notInitialized();
return;
- } else if (!emailBatch) {
+ } else if (emailBatch == null) {
parameterIsMissing("Email list");
return;
}
+ let timeout;
+ let activityData;
+ let verifyPlus;
+ if (options && typeof options === "object") {
+ timeout = options.timeout;
+ activityData = options.activityData;
+ verifyPlus = options.verifyPlus;
+ }
+
+ if (timeout != null && (timeout < 10 || timeout > 120)) {
+ parameterIsInvalid("timeout", "Must be between 10 and 120 seconds.");
+ return;
+ }
+
const body = {
api_key: this._api_key,
email_batch: emailBatch,
};
+ if (timeout != null) body.timeout = timeout;
+ if (activityData != null) body.activity_data = activityData;
+ if (verifyPlus != null) body.verify_plus = verifyPlus;
return createRequest({
requestType: "POST",
path: "/validatebatch",
@@ -206,11 +257,11 @@ export class ZeroBounceSDK {
/**
* @param email - a valid email address
* */
- getEmailActivity(email) {
+ async getEmailActivity(email) {
if (!this._initialized) {
notInitialized();
return;
- } else if (!email) {
+ } else if (isMissing(email)) {
parameterIsMissing("Email");
return;
}
@@ -226,6 +277,46 @@ export class ZeroBounceSDK {
});
}
+ /**
+ * Real-time AI scoring for a single email (GET or POST /scoring).
+ * @param email - email to score
+ * @param options - optional `{ method: "POST" }` to use form-encoded POST (default GET)
+ * */
+ async scoreEmail(email, options = null) {
+ if (!this._initialized) {
+ notInitialized();
+ return;
+ } else if (isMissing(email)) {
+ parameterIsMissing("Email");
+ return;
+ }
+
+ const params = {
+ api_key: this._api_key,
+ email,
+ };
+
+ const usePost =
+ options && typeof options === "object" && options.method === "POST";
+
+ if (usePost) {
+ return createRequest({
+ requestType: "POST",
+ path: "/scoring",
+ body: new URLSearchParams(params),
+ params: {},
+ apiBaseURL: this._api_base_url,
+ });
+ }
+
+ return createRequest({
+ requestType: "GET",
+ params,
+ path: "/scoring",
+ apiBaseURL: this._api_base_url,
+ });
+ }
+
/**
* @param file: File - The csv or txt file to be submitted.
* @param email_address_column: number - The column index of the email address in the file. Index starts from 1.
@@ -238,7 +329,7 @@ export class ZeroBounceSDK {
* @param remove_duplicate: Boolean - If you want the system to remove duplicate emails.
* @param allowPhase2: Boolean|null - Optional. When set, sends {@code allow_phase_2} (validation bulk only; see v2 send file API).
* */
- sendFile({
+ async sendFile({
file,
email_address_column,
first_name_column = false,
@@ -256,7 +347,7 @@ export class ZeroBounceSDK {
} else if (!file) {
parameterIsMissing("file");
return;
- } else if (!email_address_column) {
+ } else if (isMissing(email_address_column)) {
parameterIsMissing("email_address_column");
return;
}
@@ -280,11 +371,11 @@ export class ZeroBounceSDK {
body.append("email_address_column", email_address_column);
body.append("file", file);
- body.append("has_header_row", has_header_row);
- body.append("remove_duplicate", remove_duplicate);
+ body.append("has_header_row", booleanText(has_header_row));
+ body.append("remove_duplicate", booleanText(remove_duplicate));
body.append("api_key", this._api_key);
if (allowPhase2 != null) {
- body.append("allow_phase_2", String(allowPhase2));
+ body.append("allow_phase_2", booleanText(allowPhase2));
}
return createRequest({
@@ -302,7 +393,7 @@ export class ZeroBounceSDK {
* @param fileName: string - The file name to use for the upload (e.g. "emails.csv").
* @param options: object - Same options as sendFile (email_address_column, return_url, etc.).
* */
- sendFileStream(fileStreamOrBlob, fileName, {
+ async sendFileStream(fileStreamOrBlob, fileName, {
email_address_column,
first_name_column = false,
return_url = false,
@@ -319,10 +410,10 @@ export class ZeroBounceSDK {
} else if (!fileStreamOrBlob) {
parameterIsMissing("fileStreamOrBlob");
return;
- } else if (!fileName) {
+ } else if (isMissing(fileName)) {
parameterIsMissing("fileName");
return;
- } else if (!email_address_column) {
+ } else if (isMissing(email_address_column)) {
parameterIsMissing("email_address_column");
return;
}
@@ -335,11 +426,11 @@ export class ZeroBounceSDK {
if (ip_address_column) body.append("ip_address_column", ip_address_column);
body.append("email_address_column", email_address_column);
body.append("file", fileStreamOrBlob, fileName);
- body.append("has_header_row", has_header_row);
- body.append("remove_duplicate", remove_duplicate);
+ body.append("has_header_row", booleanText(has_header_row));
+ body.append("remove_duplicate", booleanText(remove_duplicate));
body.append("api_key", this._api_key);
if (allowPhase2 != null) {
- body.append("allow_phase_2", String(allowPhase2));
+ body.append("allow_phase_2", booleanText(allowPhase2));
}
return createRequest({
@@ -358,7 +449,7 @@ export class ZeroBounceSDK {
* @param has_header_row: Boolean - If the first row from the submitted file is a header row.
* @param remove_duplicate: Boolean - If you want the system to remove duplicate emails.
* */
- sendScoringFile({
+ async sendScoringFile({
file,
email_address_column,
return_url = false,
@@ -371,7 +462,7 @@ export class ZeroBounceSDK {
} else if (!file) {
parameterIsMissing("file: File");
return;
- } else if (!email_address_column) {
+ } else if (isMissing(email_address_column)) {
parameterIsMissing("email_address_column: number");
return;
}
@@ -384,9 +475,9 @@ export class ZeroBounceSDK {
body.append("file", file);
body.append("email_address_column", email_address_column);
- body.append("has_header_row", has_header_row);
+ body.append("has_header_row", booleanText(has_header_row));
body.append("api_key", this._api_key);
- body.append("remove_duplicate", remove_duplicate);
+ body.append("remove_duplicate", booleanText(remove_duplicate));
return createRequest({
requestType: "POST",
@@ -403,7 +494,7 @@ export class ZeroBounceSDK {
* @param fileName: string - e.g. "emails.csv"
* @param options: object - email_address_column, return_url, has_header_row, remove_duplicate
* */
- sendScoringFileStream(fileStreamOrBlob, fileName, {
+ async sendScoringFileStream(fileStreamOrBlob, fileName, {
email_address_column,
return_url = false,
has_header_row = false,
@@ -415,10 +506,10 @@ export class ZeroBounceSDK {
} else if (!fileStreamOrBlob) {
parameterIsMissing("fileStreamOrBlob");
return;
- } else if (!fileName) {
+ } else if (isMissing(fileName)) {
parameterIsMissing("fileName");
return;
- } else if (!email_address_column) {
+ } else if (isMissing(email_address_column)) {
parameterIsMissing("email_address_column");
return;
}
@@ -427,9 +518,9 @@ export class ZeroBounceSDK {
if (return_url) body.append("return_url", return_url);
body.append("file", fileStreamOrBlob, fileName);
body.append("email_address_column", email_address_column);
- body.append("has_header_row", has_header_row);
+ body.append("has_header_row", booleanText(has_header_row));
body.append("api_key", this._api_key);
- body.append("remove_duplicate", remove_duplicate);
+ body.append("remove_duplicate", booleanText(remove_duplicate));
return createRequest({
requestType: "POST",
@@ -440,11 +531,11 @@ export class ZeroBounceSDK {
});
}
- _getStatusUtil(fileId, path) {
+ async _getStatusUtil(fileId, path, { bulkRoot = false } = {}) {
if (!this._initialized) {
notInitialized();
return;
- } else if (!fileId) {
+ } else if (isMissing(fileId)) {
parameterIsMissing("File id");
return;
}
@@ -458,6 +549,7 @@ export class ZeroBounceSDK {
params,
path,
batch: true,
+ bulkBase: bulkRoot ? "root" : "v2",
apiBaseURL: this._api_base_url
});
}
@@ -465,22 +557,22 @@ export class ZeroBounceSDK {
/**
* @param fileId - the id of a previously submmitted and accepted file
* */
- getFileStatus(fileId) {
+ async getFileStatus(fileId) {
return this._getStatusUtil(fileId, "/filestatus");
}
/**
* @param fileId - the id of a previously submmitted and accepted file
* */
- getScoringFileStatus(fileId) {
+ async getScoringFileStatus(fileId) {
return this._getStatusUtil(fileId, "/scoring/filestatus");
}
- _getFileUtil(fileId, path, scoring = false, getFileOptions = null) {
+ async _getFileUtil(fileId, path, scoring = false, getFileOptions = null, { bulkRoot = false } = {}) {
if (!this._initialized) {
notInitialized();
return;
- } else if (!fileId) {
+ } else if (isMissing(fileId)) {
parameterIsMissing("File id");
return;
}
@@ -501,6 +593,7 @@ export class ZeroBounceSDK {
params,
path,
batch: true,
+ bulkBase: bulkRoot ? "root" : "v2",
returnText: true,
scoring,
apiBaseURL: this._api_base_url
@@ -511,7 +604,7 @@ export class ZeroBounceSDK {
* @param fileId - the id of a previously submmitted and accepted file
* @param getFileOptions - optional {@code downloadType} ({@link ZBDownloadType} values) and {@code activityData} (validation only)
* */
- getFile(fileId, getFileOptions = null) {
+ async getFile(fileId, getFileOptions = null) {
return this._getFileUtil(fileId, "/getfile", false, getFileOptions);
}
@@ -519,15 +612,15 @@ export class ZeroBounceSDK {
* @param fileId - the id of a previously submmitted and accepted file
* @param getFileOptions - optional {@code downloadType}; {@code activityData} is not sent for scoring getfile
* */
- getScoringFile(fileId, getFileOptions = null) {
+ async getScoringFile(fileId, getFileOptions = null) {
return this._getFileUtil(fileId, "/scoring/getfile", true, getFileOptions);
}
- _deleteFileUtil(fileId, path, scoring = false) {
+ async _deleteFileUtil(fileId, path, scoring = false, { bulkRoot = false } = {}) {
if (!this._initialized) {
notInitialized();
return;
- } else if (!fileId) {
+ } else if (isMissing(fileId)) {
parameterIsMissing("File id");
return;
}
@@ -540,25 +633,473 @@ export class ZeroBounceSDK {
params,
path,
batch: true,
+ bulkBase: bulkRoot ? "root" : "v2",
scoring,
apiBaseURL: this._api_base_url
});
}
+ async _sendBulkRootMultipartFile(path, { file, fileName = null, fields = {} }) {
+ const body = new FormData();
+ for (const [key, value] of Object.entries(fields)) {
+ if (value == null) continue;
+ body.append(key, typeof value === "boolean" ? booleanText(value) : value);
+ }
+ if (fileName != null) {
+ body.append("file", file, fileName);
+ } else {
+ body.append("file", file);
+ }
+ body.append("api_key", this._api_key);
+ return createRequest({
+ requestType: "POST",
+ path,
+ body,
+ batch: true,
+ bulkBase: "root",
+ apiBaseURL: this._api_base_url,
+ });
+ }
+
+ async _sendBulkRootFormPost(path, fields) {
+ const params = {
+ api_key: this._api_key,
+ ...fields,
+ };
+ return createRequest({
+ requestType: "POST",
+ path,
+ body: new URLSearchParams(params),
+ params: {},
+ batch: true,
+ bulkBase: "root",
+ apiBaseURL: this._api_base_url,
+ });
+ }
+
/**
* @param fileId - the id of a previously submmitted and accepted file
* */
- deleteFile(fileId) {
+ async deleteFile(fileId) {
return this._deleteFileUtil(fileId, "/deletefile");
}
/**
* @param fileId - the id of a previously submmitted and accepted file
* */
- deleteScoringFile(fileId) {
+ async deleteScoringFile(fileId) {
return this._deleteFileUtil(fileId, "/scoring/deletefile", true);
}
+ // BULK EMAIL FINDER (bulkapi.zerobounce.net/email-finder/*)
+
+ async sendEmailFinderFile({
+ file,
+ domain_column,
+ first_name_column = false,
+ last_name_column = false,
+ middle_name_column = false,
+ full_name_column = false,
+ has_header_row = false,
+ }) {
+ if (!this._initialized) {
+ notInitialized();
+ return;
+ } else if (!file) {
+ parameterIsMissing("file");
+ return;
+ } else if (isMissing(domain_column)) {
+ parameterIsMissing("domain_column");
+ return;
+ }
+ return this._sendBulkRootMultipartFile("/email-finder/sendfile", {
+ file,
+ fields: {
+ domain_column,
+ first_name_column: optionalColumn(first_name_column),
+ last_name_column: optionalColumn(last_name_column),
+ middle_name_column: optionalColumn(middle_name_column),
+ full_name_column: optionalColumn(full_name_column),
+ has_header_row: Boolean(has_header_row),
+ },
+ });
+ }
+
+ async sendEmailFinderFileStream(fileStreamOrBlob, fileName, {
+ domain_column,
+ first_name_column = false,
+ last_name_column = false,
+ middle_name_column = false,
+ full_name_column = false,
+ has_header_row = false,
+ } = {}) {
+ if (!this._initialized) {
+ notInitialized();
+ return;
+ } else if (!fileStreamOrBlob) {
+ parameterIsMissing("fileStreamOrBlob");
+ return;
+ } else if (isMissing(fileName)) {
+ parameterIsMissing("fileName");
+ return;
+ } else if (isMissing(domain_column)) {
+ parameterIsMissing("domain_column");
+ return;
+ }
+ return this._sendBulkRootMultipartFile("/email-finder/sendfile", {
+ file: fileStreamOrBlob,
+ fileName,
+ fields: {
+ domain_column,
+ first_name_column: optionalColumn(first_name_column),
+ last_name_column: optionalColumn(last_name_column),
+ middle_name_column: optionalColumn(middle_name_column),
+ full_name_column: optionalColumn(full_name_column),
+ has_header_row: Boolean(has_header_row),
+ },
+ });
+ }
+
+ async getEmailFinderFileStatus(fileId) {
+ return this._getStatusUtil(fileId, "/email-finder/filestatus", { bulkRoot: true });
+ }
+
+ async getEmailFinderFile(fileId) {
+ return this._getFileUtil(fileId, "/email-finder/getfile", false, null, { bulkRoot: true });
+ }
+
+ async deleteEmailFinderFile(fileId) {
+ return this._deleteFileUtil(fileId, "/email-finder/deletefile", false, { bulkRoot: true });
+ }
+
+ // BULK DOMAIN SEARCH (bulkapi.zerobounce.net/domain-search/*)
+
+ async sendDomainSearchFile({
+ file,
+ domain_column,
+ has_header_row = false,
+ }) {
+ if (!this._initialized) {
+ notInitialized();
+ return;
+ } else if (!file) {
+ parameterIsMissing("file");
+ return;
+ } else if (isMissing(domain_column)) {
+ parameterIsMissing("domain_column");
+ return;
+ }
+ return this._sendBulkRootMultipartFile("/domain-search/sendfile", {
+ file,
+ fields: { domain_column, has_header_row: Boolean(has_header_row) },
+ });
+ }
+
+ async sendDomainSearchFileStream(fileStreamOrBlob, fileName, {
+ domain_column,
+ has_header_row = false,
+ } = {}) {
+ if (!this._initialized) {
+ notInitialized();
+ return;
+ } else if (!fileStreamOrBlob) {
+ parameterIsMissing("fileStreamOrBlob");
+ return;
+ } else if (isMissing(fileName)) {
+ parameterIsMissing("fileName");
+ return;
+ } else if (isMissing(domain_column)) {
+ parameterIsMissing("domain_column");
+ return;
+ }
+ return this._sendBulkRootMultipartFile("/domain-search/sendfile", {
+ file: fileStreamOrBlob,
+ fileName,
+ fields: { domain_column, has_header_row: Boolean(has_header_row) },
+ });
+ }
+
+ async getDomainSearchFileStatus(fileId) {
+ return this._getStatusUtil(fileId, "/domain-search/filestatus", { bulkRoot: true });
+ }
+
+ async getDomainSearchFile(fileId) {
+ return this._getFileUtil(fileId, "/domain-search/getfile", false, null, { bulkRoot: true });
+ }
+
+ async deleteDomainSearchFile(fileId) {
+ return this._deleteFileUtil(fileId, "/domain-search/deletefile", false, { bulkRoot: true });
+ }
+
+ // LIST EVALUATOR (bulkapi.zerobounce.net/v2/listevaluator/)
+
+ async sendListEvaluatorFile({ file, email_address_column }) {
+ if (!this._initialized) {
+ notInitialized();
+ return;
+ } else if (!file) {
+ parameterIsMissing("file");
+ return;
+ } else if (isMissing(email_address_column)) {
+ parameterIsMissing("email_address_column");
+ return;
+ }
+ const body = new FormData();
+ body.append("file", file);
+ body.append("email_address_column", email_address_column);
+ body.append("api_key", this._api_key);
+ return createRequest({
+ requestType: "POST",
+ path: "/listevaluator/",
+ body,
+ batch: true,
+ apiBaseURL: this._api_base_url,
+ });
+ }
+
+ async sendListEvaluatorFileStream(fileStreamOrBlob, fileName, { email_address_column } = {}) {
+ if (!this._initialized) {
+ notInitialized();
+ return;
+ } else if (!fileStreamOrBlob) {
+ parameterIsMissing("fileStreamOrBlob");
+ return;
+ } else if (isMissing(fileName)) {
+ parameterIsMissing("fileName");
+ return;
+ } else if (isMissing(email_address_column)) {
+ parameterIsMissing("email_address_column");
+ return;
+ }
+ const body = new FormData();
+ body.append("file", fileStreamOrBlob, fileName);
+ body.append("email_address_column", email_address_column);
+ body.append("api_key", this._api_key);
+ return createRequest({
+ requestType: "POST",
+ path: "/listevaluator/",
+ body,
+ batch: true,
+ apiBaseURL: this._api_base_url,
+ });
+ }
+
+ async getListEvaluatorResult(fileId) {
+ if (!this._initialized) {
+ notInitialized();
+ return;
+ } else if (isMissing(fileId)) {
+ parameterIsMissing("File id");
+ return;
+ }
+ const params = { api_key: this._api_key };
+ return createRequest({
+ requestType: "GET",
+ params,
+ path: `/listevaluator/${fileId}/`,
+ batch: true,
+ apiBaseURL: this._api_base_url,
+ });
+ }
+
+ async sendEvaluatedFile({ file }) {
+ if (!this._initialized) {
+ notInitialized();
+ return;
+ } else if (!file) {
+ parameterIsMissing("file");
+ return;
+ }
+ const body = new FormData();
+ body.append("file", file);
+ body.append("api_key", this._api_key);
+ return createRequest({
+ requestType: "POST",
+ path: "/send-evaluated-file/",
+ body,
+ batch: true,
+ apiBaseURL: this._api_base_url,
+ });
+ }
+
+ // CUSTOM FILTERS (main API /v2/filters/*)
+
+ async addFilter({ rule, target, value }) {
+ if (!this._initialized) {
+ notInitialized();
+ return;
+ } else if (isMissing(rule)) {
+ parameterIsMissing("rule");
+ return;
+ } else if (isMissing(target)) {
+ parameterIsMissing("target");
+ return;
+ } else if (isMissing(value)) {
+ parameterIsMissing("value");
+ return;
+ }
+ const params = {
+ api_key: this._api_key,
+ rule,
+ target,
+ value,
+ };
+ return createRequest({
+ requestType: "POST",
+ path: "/filters/add",
+ body: new URLSearchParams(params),
+ params: {},
+ apiBaseURL: this._api_base_url,
+ });
+ }
+
+ async listFilters(options = null) {
+ if (!this._initialized) {
+ notInitialized();
+ return;
+ }
+ const params = { api_key: this._api_key };
+ const usePost =
+ options && typeof options === "object" && options.method === "POST";
+ if (usePost) {
+ return createRequest({
+ requestType: "POST",
+ path: "/filters/list",
+ body: new URLSearchParams(params),
+ params: {},
+ apiBaseURL: this._api_base_url,
+ });
+ }
+ return createRequest({
+ requestType: "GET",
+ params,
+ path: "/filters/list",
+ apiBaseURL: this._api_base_url,
+ });
+ }
+
+ async deleteFilter({ rule, target, value }) {
+ if (!this._initialized) {
+ notInitialized();
+ return;
+ } else if (isMissing(rule)) {
+ parameterIsMissing("rule");
+ return;
+ } else if (isMissing(target)) {
+ parameterIsMissing("target");
+ return;
+ } else if (isMissing(value)) {
+ parameterIsMissing("value");
+ return;
+ }
+ const params = {
+ api_key: this._api_key,
+ rule,
+ target,
+ value,
+ };
+ return createRequest({
+ requestType: "POST",
+ path: "/filters/delete",
+ body: new URLSearchParams(params),
+ params: {},
+ apiBaseURL: this._api_base_url,
+ });
+ }
+
+ // AMAZON S3 BULK HELPERS (bulkapi root)
+
+ async listS3files({ bucket = null, search = null } = {}) {
+ if (!this._initialized) {
+ notInitialized();
+ return;
+ }
+ const params = { api_key: this._api_key };
+ if (bucket != null) params.bucket = bucket;
+ if (search != null) params.search = search;
+ return createRequest({
+ requestType: "GET",
+ params,
+ path: "/listS3files",
+ batch: true,
+ bulkBase: "root",
+ apiBaseURL: this._api_base_url,
+ });
+ }
+
+ async validateS3file({
+ file_name,
+ has_header_row,
+ remove_duplicates,
+ email_address_column = null,
+ return_url = false,
+ first_name_column = false,
+ last_name_column = false,
+ gender_column = false,
+ ip_address_column = false,
+ remove_duplicate = null,
+ allowPhase2 = null,
+ }) {
+ if (!this._initialized) {
+ notInitialized();
+ return;
+ } else if (isMissing(file_name)) {
+ parameterIsMissing("file_name");
+ return;
+ }
+ const fields = { file_name };
+ if (has_header_row != null) fields.has_header_row = has_header_row;
+ if (remove_duplicates != null) fields.remove_duplicates = remove_duplicates;
+ if (remove_duplicate != null) fields.remove_duplicate = remove_duplicate;
+ if (email_address_column != null) fields.email_address_column = email_address_column;
+ if (return_url) fields.return_url = return_url;
+ if (first_name_column) fields.first_name_column = first_name_column;
+ if (last_name_column) fields.last_name_column = last_name_column;
+ if (gender_column) fields.gender_column = gender_column;
+ if (ip_address_column) fields.ip_address_column = ip_address_column;
+ if (allowPhase2 != null) fields.allow_phase_2 = String(allowPhase2);
+ return this._sendBulkRootFormPost("/validateS3file", fields);
+ }
+
+ async scoreS3file({
+ file_name,
+ has_header_row,
+ remove_duplicates,
+ email_address_column = null,
+ remove_duplicate = null,
+ }) {
+ if (!this._initialized) {
+ notInitialized();
+ return;
+ } else if (isMissing(file_name)) {
+ parameterIsMissing("file_name");
+ return;
+ }
+ const fields = { file_name };
+ if (has_header_row != null) fields.has_header_row = has_header_row;
+ if (remove_duplicates != null) fields.remove_duplicates = remove_duplicates;
+ if (remove_duplicate != null) fields.remove_duplicate = remove_duplicate;
+ if (email_address_column != null) fields.email_address_column = email_address_column;
+ return this._sendBulkRootFormPost("/scoreS3file", fields);
+ }
+
+ async exportS3file({ file_id, export_bucket }) {
+ if (!this._initialized) {
+ notInitialized();
+ return;
+ } else if (isMissing(file_id)) {
+ parameterIsMissing("file_id");
+ return;
+ } else if (isMissing(export_bucket)) {
+ parameterIsMissing("export_bucket");
+ return;
+ }
+ return this._sendBulkRootFormPost("/exportS3file", {
+ file_id,
+ export_bucket,
+ });
+ }
+
// EMAIL FINDER
/**
@@ -567,7 +1108,7 @@ export class ZeroBounceSDK {
* @param middle_name str or null - middle name
* @param last_name str or null - last name
* */
- findEmailByDomain({
+ async findEmailByDomain({
domain,
first_name,
middle_name = null,
@@ -587,7 +1128,7 @@ export class ZeroBounceSDK {
* @param middle_name str or null - middle name
* @param last_name str or null - last name
* */
- findEmailByCompanyName({
+ async findEmailByCompanyName({
company_name,
first_name,
middle_name = null,
@@ -601,7 +1142,7 @@ export class ZeroBounceSDK {
});
}
- _findEmail({
+ async _findEmail({
domain = null,
company_name = null,
first_name,
@@ -611,11 +1152,11 @@ export class ZeroBounceSDK {
if (!this._initialized) {
notInitialized();
return;
- } else if (!domain && !company_name) {
+ } else if (isMissing(domain) && isMissing(company_name)) {
parameterIsMissing("domain");
parameterIsMissing("company_name");
return;
- } else if (!first_name) {
+ } else if (isMissing(first_name)) {
parameterIsMissing("first_name");
return;
}
@@ -623,9 +1164,9 @@ export class ZeroBounceSDK {
const params = {
api_key: this._api_key,
first_name: first_name,
- middle_name: middle_name,
- last_name: last_name,
};
+ if (!isMissing(middle_name)) params.middle_name = middle_name;
+ if (!isMissing(last_name)) params.last_name = last_name;
if (domain != null) {
params["domain"] = domain;
@@ -647,7 +1188,7 @@ export class ZeroBounceSDK {
/**
* @param domain str - domain name
* */
- findEmailFormatByDomain({
+ async findEmailFormatByDomain({
domain
}) {
return this._findEmailFormat({
@@ -658,7 +1199,7 @@ export class ZeroBounceSDK {
/**
* @param company_name str - company name
* */
- findEmailFormatByCompanyName({
+ async findEmailFormatByCompanyName({
company_name
}) {
return this._findEmailFormat({
@@ -666,14 +1207,14 @@ export class ZeroBounceSDK {
});
}
- _findEmailFormat({
+ async _findEmailFormat({
domain = null,
company_name = null
}) {
if (!this._initialized) {
notInitialized();
return;
- } else if (!domain && !company_name) {
+ } else if (isMissing(domain) && isMissing(company_name)) {
parameterIsMissing("domain");
parameterIsMissing("company_name");
return;
@@ -705,7 +1246,7 @@ export class ZeroBounceSDK {
* @param middle_name str or null - middle name
* @param last_name str or null - last name
* */
- guessFormat ({
+ async guessFormat ({
domain,
first_name = null,
middle_name = null,
@@ -715,7 +1256,7 @@ export class ZeroBounceSDK {
if (!this._initialized) {
notInitialized();
return;
- } else if (!domain) {
+ } else if (isMissing(domain)) {
parameterIsMissing("domain");
return;
}
@@ -723,10 +1264,10 @@ export class ZeroBounceSDK {
const params = {
api_key: this._api_key,
domain: domain,
- first_name: first_name,
- middle_name: middle_name,
- last_name: last_name,
};
+ if (!isMissing(first_name)) params.first_name = first_name;
+ if (!isMissing(middle_name)) params.middle_name = middle_name;
+ if (!isMissing(last_name)) params.last_name = last_name;
return createRequest({
requestType: "GET",
diff --git a/tests/offline-api.test.js b/tests/offline-api.test.js
new file mode 100644
index 0000000..e6a6cd6
--- /dev/null
+++ b/tests/offline-api.test.js
@@ -0,0 +1,727 @@
+/**
+ * Offline API suite. Replaces global fetch; no network and no real API key.
+ */
+import { ZeroBounceSDK } from "../src/zero-bounce.js";
+
+const HTML_PAGE = (() => {
+ const head =
+ '
Access denied | zerobounce.net | Zero Bounce';
+ return `${head}${"blocked
".repeat(20000)}`;
+})();
+
+const CF_JSON = JSON.stringify({
+ type: "https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1020/",
+ title: "Error 1020: Access denied",
+ status: 403,
+ detail: "The request was blocked by a Cloudflare firewall rule configured by the site owner.",
+ error_code: 1020,
+ cloudflare_error: true,
+ what_you_should_do: "Do not retry. This block is intentional.",
+});
+
+function delay(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+function flattenJson(value) {
+ const fields = {};
+ if (!value || typeof value !== "object" || Array.isArray(value)) return fields;
+ for (const [key, item] of Object.entries(value)) {
+ if (item == null) fields[key] = "null";
+ else if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
+ fields[key] = String(item);
+ } else {
+ fields[key] = JSON.stringify(item);
+ }
+ }
+ return fields;
+}
+
+async function readBody(body) {
+ if (body == null) return { fields: {}, json: null };
+ if (typeof body === "string") {
+ const trimmed = body.trim();
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
+ const json = JSON.parse(trimmed);
+ return { fields: flattenJson(json), json };
+ }
+ const params = new URLSearchParams(body);
+ return { fields: Object.fromEntries(params.entries()), json: null };
+ }
+ if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) {
+ return { fields: Object.fromEntries(body.entries()), json: null };
+ }
+ if (typeof FormData !== "undefined" && body instanceof FormData) {
+ const fields = {};
+ for (const [key, value] of body.entries()) {
+ fields[key] = typeof value === "string" ? value : "";
+ }
+ return { fields, json: null };
+ }
+ return { fields: {}, json: null };
+}
+
+function httpResponse(status, body, contentType) {
+ return {
+ status,
+ ok: status >= 200 && status < 300,
+ headers: {
+ get(name) {
+ return String(name).toLowerCase() === "content-type" ? contentType : null;
+ },
+ },
+ text: async () => body,
+ json: async () => JSON.parse(body),
+ };
+}
+
+function successBody(request) {
+ const key = JSON.stringify(request.apiKey);
+ const path = request.path;
+ const ends = (suffix) => path.endsWith(suffix);
+ if (path.includes("getfile")) {
+ return {
+ status: 200,
+ contentType: "text/csv",
+ body: `email\n${request.apiKey}@example.com\n`,
+ };
+ }
+ if (path.includes("validatebatch")) {
+ return {
+ status: 200,
+ contentType: "application/json",
+ body: `{"email_batch":[{"address":${key},"status":"valid"}],"errors":[],"echo_api_key":${key}}`,
+ };
+ }
+ if (ends("/validate")) {
+ return {
+ status: 200,
+ contentType: "application/json",
+ body: `{"address":"person@example.com","status":"valid","echo_api_key":${key}}`,
+ };
+ }
+ if (ends("/scoring")) {
+ return {
+ status: 200,
+ contentType: "application/json",
+ body: `{"email":"person@example.com","score":10,"echo_api_key":${key}}`,
+ };
+ }
+ if (path.includes("getcredits")) {
+ return {
+ status: 200,
+ contentType: "application/json",
+ body: `{"Credits":"7","echo_api_key":${key}}`,
+ };
+ }
+ if (path.includes("getapiusage")) {
+ return {
+ status: 200,
+ contentType: "application/json",
+ body: `{"total":1,"echo_api_key":${key}}`,
+ };
+ }
+ if (ends("/activity")) {
+ return {
+ status: 200,
+ contentType: "application/json",
+ body: `{"found":true,"active_in_days":"3","echo_api_key":${key}}`,
+ };
+ }
+ if (path.includes("guessformat")) {
+ return {
+ status: 200,
+ contentType: "application/json",
+ body: `{"email":"ada@example.com","domain":"example.com","echo_api_key":${key},"other_domain_formats":[]}`,
+ };
+ }
+ return {
+ status: 200,
+ contentType: "application/json",
+ body: `{"success":true,"message":"ok","file_id":${key},"echo_api_key":${key}}`,
+ };
+}
+
+function answer(request) {
+ const key = request.apiKey;
+ if (key.startsWith("html-403-") || key.startsWith("html-200-")) {
+ return httpResponse(key.startsWith("html-403-") ? 403 : 200, HTML_PAGE, "text/html; charset=UTF-8");
+ }
+ if (key.startsWith("empty-")) {
+ return httpResponse(200, "", "application/json");
+ }
+ if (key.startsWith("malformed-")) {
+ return httpResponse(200, `<<>>${"x".repeat(5000)}`, "application/json");
+ }
+ if (key.startsWith("cf-json-")) {
+ return httpResponse(403, CF_JSON, "application/json");
+ }
+ if (key.startsWith("double-error-")) {
+ return httpResponse(400, "Error: Error: api_key is invalid", "text/plain");
+ }
+ if (key.startsWith("invalid-key-")) {
+ return httpResponse(200, JSON.stringify({ error: "Invalid API Key" }), "application/json");
+ }
+ if (key.startsWith("no-credits-")) {
+ return httpResponse(
+ 200,
+ JSON.stringify({ error: "Your account ran out of credits" }),
+ "application/json"
+ );
+ }
+ if (pathIsBatch(request) && key.startsWith("batch-account-")) {
+ return httpResponse(
+ 200,
+ JSON.stringify({
+ email_batch: [],
+ errors: [{ email_address: "all", error: "Invalid API Key or your account ran out of credits" }],
+ }),
+ "application/json"
+ );
+ }
+ if (pathIsBatch(request) && key.startsWith("batch-partial-")) {
+ return httpResponse(
+ 200,
+ JSON.stringify({
+ email_batch: [{ address: "good@example.com", status: "valid" }],
+ errors: [{ email_address: "bad@", error: "Invalid email address" }],
+ }),
+ "application/json"
+ );
+ }
+ if (key.startsWith("score-item-")) {
+ return httpResponse(200, JSON.stringify({ error: "invalid email" }), "application/json");
+ }
+ const success = successBody(request);
+ return httpResponse(success.status, success.body, success.contentType);
+}
+
+function pathIsBatch(request) {
+ return request.path.includes("validatebatch");
+}
+
+function installFakeFetch() {
+ const records = [];
+ const previous = global.fetch;
+ global.fetch = async (url, init = {}) => {
+ const parsed = new URL(String(url));
+ const body = await readBody(init.body);
+ const query = Object.fromEntries(parsed.searchParams.entries());
+ const recorded = {
+ method: String(init.method || "GET").toUpperCase(),
+ host: parsed.host,
+ path: parsed.pathname,
+ url: String(url),
+ query,
+ body: body.fields,
+ json: body.json,
+ fields: { ...query, ...body.fields },
+ apiKey: body.fields.api_key || query.api_key || "",
+ redirect: init.redirect,
+ };
+ records.push(recorded);
+ await delay(Math.floor(Math.random() * 21));
+ return answer(recorded);
+ };
+ return {
+ records,
+ restore() {
+ global.fetch = previous;
+ },
+ only() {
+ expect(records).toHaveLength(1);
+ return records[0];
+ },
+ };
+}
+
+function expectShort(message) {
+ expect(typeof message).toBe("string");
+ expect(message.length).toBeLessThan(500);
+ expect(message).not.toMatch(/blocked/i);
+ expect(message).not.toMatch(/Error:\s*Error:/);
+ expect(message).not.toMatch(/\[Error\]:/);
+ expect(message.startsWith("Error:")).toBe(false);
+ expect(message).not.toMatch(/SyntaxError|Unexpected token|json\.exception/i);
+ expect(message).not.toContain("<<>>");
+}
+
+async function expectRejected(promise) {
+ let thrown = null;
+ try {
+ await promise;
+ } catch (error) {
+ thrown = error;
+ }
+ expect(thrown).toBeInstanceOf(Error);
+ expectShort(thrown.message);
+ return thrown;
+}
+
+function client(key, base = ZeroBounceSDK.ApiURL.DEFAULT_API_URL) {
+ const sdk = new ZeroBounceSDK();
+ sdk.init(key, base);
+ return sdk;
+}
+
+describe("offline API", () => {
+ let fake;
+ const file = new File(["email\n"], "emails.csv", { type: "text/csv" });
+
+ beforeEach(() => {
+ fake = installFakeFetch();
+ jest.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ fake.restore();
+ });
+
+ function callAll(sdk) {
+ const id = "file-1";
+ return [
+ ["getCredits", () => sdk.getCredits(), "GET", "api.zerobounce.net", "/v2/getcredits"],
+ ["validateEmail", () => sdk.validateEmail("a@example.com"), "GET", "api.zerobounce.net", "/v2/validate"],
+ ["getApiUsage", () => sdk.getApiUsage("2020-01-01", "2020-01-31"), "GET", "api.zerobounce.net", "/v2/getapiusage"],
+ ["validateBatch", () => sdk.validateBatch([{ email_address: "a@example.com" }]), "POST", "api.zerobounce.net", "/v2/validatebatch"],
+ ["getEmailActivity", () => sdk.getEmailActivity("a@example.com"), "GET", "api.zerobounce.net", "/v2/activity"],
+ ["scoreEmail", () => sdk.scoreEmail("a@example.com"), "GET", "api.zerobounce.net", "/v2/scoring"],
+ ["sendFile", () => sdk.sendFile({ file, email_address_column: 1 }), "POST", "bulkapi.zerobounce.net", "/v2/sendfile"],
+ ["sendFileStream", () => sdk.sendFileStream(file, "emails.csv", { email_address_column: 1 }), "POST", "bulkapi.zerobounce.net", "/v2/sendfile"],
+ ["sendScoringFile", () => sdk.sendScoringFile({ file, email_address_column: 1 }), "POST", "bulkapi.zerobounce.net", "/v2/scoring/sendfile"],
+ ["sendScoringFileStream", () => sdk.sendScoringFileStream(file, "emails.csv", { email_address_column: 1 }), "POST", "bulkapi.zerobounce.net", "/v2/scoring/sendfile"],
+ ["getFileStatus", () => sdk.getFileStatus(id), "GET", "bulkapi.zerobounce.net", "/v2/filestatus"],
+ ["getScoringFileStatus", () => sdk.getScoringFileStatus(id), "GET", "bulkapi.zerobounce.net", "/v2/scoring/filestatus"],
+ ["getFile", () => sdk.getFile(id), "GET", "bulkapi.zerobounce.net", "/v2/getfile"],
+ ["getScoringFile", () => sdk.getScoringFile(id), "GET", "bulkapi.zerobounce.net", "/v2/scoring/getfile"],
+ ["deleteFile", () => sdk.deleteFile(id), "GET", "bulkapi.zerobounce.net", "/v2/deletefile"],
+ ["deleteScoringFile", () => sdk.deleteScoringFile(id), "GET", "bulkapi.zerobounce.net", "/v2/scoring/deletefile"],
+ ["sendEmailFinderFile", () => sdk.sendEmailFinderFile({ file, domain_column: 1 }), "POST", "bulkapi.zerobounce.net", "/email-finder/sendfile"],
+ ["sendEmailFinderFileStream", () => sdk.sendEmailFinderFileStream(file, "emails.csv", { domain_column: 1 }), "POST", "bulkapi.zerobounce.net", "/email-finder/sendfile"],
+ ["getEmailFinderFileStatus", () => sdk.getEmailFinderFileStatus(id), "GET", "bulkapi.zerobounce.net", "/email-finder/filestatus"],
+ ["getEmailFinderFile", () => sdk.getEmailFinderFile(id), "GET", "bulkapi.zerobounce.net", "/email-finder/getfile"],
+ ["deleteEmailFinderFile", () => sdk.deleteEmailFinderFile(id), "GET", "bulkapi.zerobounce.net", "/email-finder/deletefile"],
+ ["sendDomainSearchFile", () => sdk.sendDomainSearchFile({ file, domain_column: 1 }), "POST", "bulkapi.zerobounce.net", "/domain-search/sendfile"],
+ ["sendDomainSearchFileStream", () => sdk.sendDomainSearchFileStream(file, "emails.csv", { domain_column: 1 }), "POST", "bulkapi.zerobounce.net", "/domain-search/sendfile"],
+ ["getDomainSearchFileStatus", () => sdk.getDomainSearchFileStatus(id), "GET", "bulkapi.zerobounce.net", "/domain-search/filestatus"],
+ ["getDomainSearchFile", () => sdk.getDomainSearchFile(id), "GET", "bulkapi.zerobounce.net", "/domain-search/getfile"],
+ ["deleteDomainSearchFile", () => sdk.deleteDomainSearchFile(id), "GET", "bulkapi.zerobounce.net", "/domain-search/deletefile"],
+ ["sendListEvaluatorFile", () => sdk.sendListEvaluatorFile({ file, email_address_column: 1 }), "POST", "bulkapi.zerobounce.net", "/v2/listevaluator/"],
+ ["sendListEvaluatorFileStream", () => sdk.sendListEvaluatorFileStream(file, "emails.csv", { email_address_column: 1 }), "POST", "bulkapi.zerobounce.net", "/v2/listevaluator/"],
+ ["getListEvaluatorResult", () => sdk.getListEvaluatorResult(id), "GET", "bulkapi.zerobounce.net", "/v2/listevaluator/file-1/"],
+ ["sendEvaluatedFile", () => sdk.sendEvaluatedFile({ file }), "POST", "bulkapi.zerobounce.net", "/v2/send-evaluated-file/"],
+ ["addFilter", () => sdk.addFilter({ rule: "exclude", target: "domain", value: "spam.com" }), "POST", "api.zerobounce.net", "/v2/filters/add"],
+ ["listFilters", () => sdk.listFilters(), "GET", "api.zerobounce.net", "/v2/filters/list"],
+ ["deleteFilter", () => sdk.deleteFilter({ rule: "exclude", target: "domain", value: "spam.com" }), "POST", "api.zerobounce.net", "/v2/filters/delete"],
+ ["listS3files", () => sdk.listS3files(), "GET", "bulkapi.zerobounce.net", "/listS3files"],
+ ["validateS3file", () => sdk.validateS3file({ file_name: "f.csv" }), "POST", "bulkapi.zerobounce.net", "/validateS3file"],
+ ["scoreS3file", () => sdk.scoreS3file({ file_name: "f.csv" }), "POST", "bulkapi.zerobounce.net", "/scoreS3file"],
+ ["exportS3file", () => sdk.exportS3file({ file_id: id, export_bucket: "bucket" }), "POST", "bulkapi.zerobounce.net", "/exportS3file"],
+ ["findEmailByDomain", () => sdk.findEmailByDomain({ domain: "example.com", first_name: "Ada" }), "GET", "api.zerobounce.net", "/v2/guessformat"],
+ ["findEmailByCompanyName", () => sdk.findEmailByCompanyName({ company_name: "Ada Inc", first_name: "Ada" }), "GET", "api.zerobounce.net", "/v2/guessformat"],
+ ["findEmailFormatByDomain", () => sdk.findEmailFormatByDomain({ domain: "example.com" }), "GET", "api.zerobounce.net", "/v2/guessformat"],
+ ["findEmailFormatByCompanyName", () => sdk.findEmailFormatByCompanyName({ company_name: "Ada Inc" }), "GET", "api.zerobounce.net", "/v2/guessformat"],
+ ["guessFormat", () => sdk.guessFormat({ domain: "example.com" }), "GET", "api.zerobounce.net", "/v2/guessformat"],
+ ];
+ }
+
+ it("calls the right host, path, and method for every public method", async () => {
+ const sdk = client("parity-key");
+ jest.spyOn(console, "warn").mockImplementation(() => {});
+ for (const [name, run, method, host, path] of callAll(sdk)) {
+ fake.records.length = 0;
+ await run();
+ expect({ name, method: fake.records[0].method, host: fake.records[0].host, path: fake.records[0].path }).toEqual({
+ name,
+ method,
+ host,
+ path,
+ });
+ expect(fake.records[0].apiKey).toBe("parity-key");
+ }
+ });
+
+ it("uses the regional host selected in init", async () => {
+ const usa = client("parity-key", ZeroBounceSDK.ApiURL.USA_API_URL);
+ await usa.getCredits();
+ expect(fake.only().host).toBe("api-us.zerobounce.net");
+ fake.records.length = 0;
+ const eu = client("parity-key", ZeroBounceSDK.ApiURL.EU_API_URL);
+ await eu.getCredits();
+ expect(fake.only().host).toBe("api-eu.zerobounce.net");
+ });
+
+ it("does not follow redirects", async () => {
+ const sdk = client("parity-key");
+ await sdk.getCredits();
+ expect(fake.only().redirect).toBe("error");
+ });
+
+ it("percent-encodes query values", async () => {
+ const sdk = client("key with space");
+ await sdk.validateEmail("a&b=c@example.com");
+ const request = fake.only();
+ expect(request.url).not.toContain("a&b=c@example.com");
+ expect(request.url).not.toContain("key with space");
+ expect(request.fields.email).toBe("a&b=c@example.com");
+ expect(request.fields.api_key).toBe("key with space");
+ });
+
+ it("omits unset validate options and sends false and enums as the API expects", async () => {
+ const sdk = client("parity-key");
+ await sdk.validateEmail("a@example.com");
+ let request = fake.only();
+ expect(request.fields.timeout).toBeUndefined();
+ expect(request.fields.activity_data).toBeUndefined();
+ expect(request.fields.verify_plus).toBeUndefined();
+ expect(request.fields.ip_address).toBeUndefined();
+
+ fake.records.length = 0;
+ await sdk.validateEmail("a@example.com", {
+ ip_address: "127.0.0.1",
+ timeout: 30,
+ activityData: false,
+ verifyPlus: true,
+ });
+ request = fake.only();
+ expect(request.fields.ip_address).toBe("127.0.0.1");
+ expect(request.fields.timeout).toBe("30");
+ expect(request.fields.activity_data).toBe("false");
+ expect(request.fields.verify_plus).toBe("true");
+
+ fake.records.length = 0;
+ await sdk.validateEmail("a@example.com", { method: "POST", verifyPlus: false });
+ request = fake.only();
+ expect(request.method).toBe("POST");
+ expect(request.query.api_key).toBeUndefined();
+ expect(request.body.api_key).toBe("parity-key");
+ expect(request.body.verify_plus).toBe("false");
+ expect(request.body.activity_data).toBeUndefined();
+ });
+
+ it("posts validateBatch to the JSON API and keeps partial errors on the result", async () => {
+ const sdk = client("batch-partial-key");
+ const batch = [{ email_address: "good@example.com", ip_address: "127.0.0.1" }];
+ const result = await sdk.validateBatch(batch, { activityData: false, verifyPlus: true });
+ const request = fake.only();
+ expect(request.host).toBe("api.zerobounce.net");
+ expect(request.path).toBe("/v2/validatebatch");
+ expect(request.json.activity_data).toBe(false);
+ expect(request.json.verify_plus).toBe(true);
+ expect(request.json.timeout).toBeUndefined();
+ expect(request.json.email_batch).toEqual(batch);
+ expect(result.email_batch).toEqual([{ address: "good@example.com", status: "valid" }]);
+ expect(result.errors).toEqual([{ email_address: "bad@", error: "Invalid email address" }]);
+ });
+
+ it("rejects a validateBatch account-level error", async () => {
+ const sdk = client("batch-account-key");
+ const error = await expectRejected(
+ sdk.validateBatch([{ email_address: "a@example.com" }])
+ );
+ expect(error.message).toBe("Invalid API Key or your account ran out of credits");
+ });
+
+ it("rejects account-level invalid key and no-credits responses", async () => {
+ const invalid = await expectRejected(client("invalid-key-1").getCredits());
+ expect(invalid.message).toBe("Invalid API Key");
+ fake.records.length = 0;
+ const credits = await expectRejected(client("no-credits-1").getCredits());
+ expect(credits.message).toBe("Your account ran out of credits");
+ });
+
+ it("leaves a per-email scoring error on the resolved result", async () => {
+ const sdk = client("score-item-1");
+ await expect(sdk.scoreEmail("not-an-email")).resolves.toEqual({ error: "invalid email" });
+ });
+
+ it("sends scoreEmail and listFilters as form posts when method is POST", async () => {
+ const sdk = client("parity-key");
+ await sdk.scoreEmail("a@example.com", { method: "POST" });
+ let request = fake.only();
+ expect(request.method).toBe("POST");
+ expect(request.path).toBe("/v2/scoring");
+ expect(request.query.api_key).toBeUndefined();
+ expect(request.body).toEqual({ api_key: "parity-key", email: "a@example.com" });
+
+ fake.records.length = 0;
+ await sdk.listFilters({ method: "POST" });
+ request = fake.only();
+ expect(request.method).toBe("POST");
+ expect(request.path).toBe("/v2/filters/list");
+ expect(request.body.api_key).toBe("parity-key");
+ expect(request.query.api_key).toBeUndefined();
+ });
+
+ it("sends bulk booleans as true or false, including an explicit false header row", async () => {
+ const sdk = client("parity-key");
+ await sdk.sendFile({
+ file,
+ email_address_column: 1,
+ has_header_row: false,
+ remove_duplicate: true,
+ allowPhase2: false,
+ });
+ let request = fake.only();
+ expect(request.fields.has_header_row).toBe("false");
+ expect(request.fields.remove_duplicate).toBe("true");
+ expect(request.fields.allow_phase_2).toBe("false");
+
+ fake.records.length = 0;
+ await sdk.sendFile({ file, email_address_column: 1 });
+ request = fake.only();
+ expect(request.fields.allow_phase_2).toBeUndefined();
+ expect(request.fields.has_header_row).toBe("false");
+ expect(request.fields.remove_duplicate).toBe("false");
+
+ fake.records.length = 0;
+ await sdk.sendScoringFile({ file, email_address_column: 1, has_header_row: false, remove_duplicate: false });
+ request = fake.only();
+ expect(request.fields.allow_phase_2).toBeUndefined();
+ expect(request.fields.has_header_row).toBe("false");
+ expect(request.fields.remove_duplicate).toBe("false");
+ expect(request.fields.first_name_column).toBeUndefined();
+
+ fake.records.length = 0;
+ await sdk.sendEmailFinderFile({ file, domain_column: 2, has_header_row: false, first_name_column: 0 });
+ request = fake.only();
+ expect(request.fields.has_header_row).toBe("false");
+ expect(request.fields.domain_column).toBe("2");
+ expect(request.fields.first_name_column).toBeUndefined();
+
+ fake.records.length = 0;
+ await sdk.sendDomainSearchFile({ file, domain_column: 1, has_header_row: false });
+ expect(fake.only().fields.has_header_row).toBe("false");
+ });
+
+ it("sends column index 0 for required columns and leaves optional 0 columns out", async () => {
+ const sdk = client("parity-key");
+ await sdk.sendFile({
+ file,
+ email_address_column: 0,
+ first_name_column: 0,
+ last_name_column: 3,
+ });
+ let request = fake.only();
+ expect(request.fields.email_address_column).toBe("0");
+ expect(request.fields.first_name_column).toBeUndefined();
+ expect(request.fields.last_name_column).toBe("3");
+
+ fake.records.length = 0;
+ await sdk.sendEmailFinderFile({ file, domain_column: 0 });
+ expect(fake.only().fields.domain_column).toBe("0");
+
+ fake.records.length = 0;
+ await sdk.sendListEvaluatorFile({ file, email_address_column: 0 });
+ expect(fake.only().fields.email_address_column).toBe("0");
+
+ fake.records.length = 0;
+ await sdk.validateS3file({
+ file_name: "f.csv",
+ email_address_column: 0,
+ first_name_column: 0,
+ has_header_row: false,
+ remove_duplicate: false,
+ });
+ request = fake.only();
+ expect(request.fields.email_address_column).toBe("0");
+ expect(request.fields.first_name_column).toBeUndefined();
+ expect(request.fields.has_header_row).toBe("false");
+ expect(request.fields.remove_duplicate).toBe("false");
+ expect(request.fields.allow_phase_2).toBeUndefined();
+ });
+
+ it("sends getFile download_type and activity_data only when set, and not for scoring", async () => {
+ const sdk = client("parity-key");
+ await sdk.getFile("file-1", {
+ downloadType: ZeroBounceSDK.ZBDownloadType.PHASE_1,
+ activityData: false,
+ });
+ let request = fake.only();
+ expect(request.fields.download_type).toBe("phase_1");
+ expect(request.fields.activity_data).toBe("false");
+
+ fake.records.length = 0;
+ await sdk.getFile("file-1");
+ request = fake.only();
+ expect(request.fields.download_type).toBeUndefined();
+ expect(request.fields.activity_data).toBeUndefined();
+
+ fake.records.length = 0;
+ await sdk.getScoringFile("file-1", {
+ downloadType: ZeroBounceSDK.ZBDownloadType.COMBINED,
+ activityData: true,
+ });
+ request = fake.only();
+ expect(request.fields.download_type).toBe("combined");
+ expect(request.fields.activity_data).toBeUndefined();
+ });
+
+ it("omits unset email-finder names and does not send the string null", async () => {
+ const sdk = client("parity-key");
+ jest.spyOn(console, "warn").mockImplementation(() => {});
+ await sdk.findEmailByDomain({ domain: "example.com", first_name: "Ada" });
+ let request = fake.only();
+ expect(request.fields.domain).toBe("example.com");
+ expect(request.fields.first_name).toBe("Ada");
+ expect(request.fields.middle_name).toBeUndefined();
+ expect(request.fields.last_name).toBeUndefined();
+ expect(request.fields.company_name).toBeUndefined();
+ expect(request.url).not.toContain("middle_name");
+ expect(request.url).not.toContain("=null");
+
+ fake.records.length = 0;
+ await sdk.findEmailByCompanyName({ company_name: "Ada Inc", first_name: "Ada", last_name: "Lovelace" });
+ request = fake.only();
+ expect(request.fields.company_name).toBe("Ada Inc");
+ expect(request.fields.last_name).toBe("Lovelace");
+ expect(request.fields.domain).toBeUndefined();
+ expect(request.fields.middle_name).toBeUndefined();
+
+ fake.records.length = 0;
+ await sdk.guessFormat({ domain: "example.com" });
+ request = fake.only();
+ expect(request.fields.domain).toBe("example.com");
+ expect(request.fields.first_name).toBeUndefined();
+ expect(request.fields.middle_name).toBeUndefined();
+ expect(request.fields.last_name).toBeUndefined();
+ });
+
+ it("omits unset listS3files and filter fields", async () => {
+ const sdk = client("parity-key");
+ await sdk.listS3files();
+ expect(fake.only().fields.bucket).toBeUndefined();
+ expect(fake.records[0].fields.search).toBeUndefined();
+ fake.records.length = 0;
+ await sdk.listS3files({ bucket: "inbox", search: "a.csv" });
+ expect(fake.only().fields.bucket).toBe("inbox");
+ expect(fake.records[0].fields.search).toBe("a.csv");
+ fake.records.length = 0;
+ await sdk.addFilter({ rule: "include", target: "email", value: "a@example.com" });
+ expect(fake.only().body).toEqual({
+ api_key: "parity-key",
+ rule: "include",
+ target: "email",
+ value: "a@example.com",
+ });
+ });
+
+ it("rejects missing and invalid arguments before any request", async () => {
+ const bare = new ZeroBounceSDK();
+ await expectRejected(bare.getCredits());
+ expect(fake.records).toHaveLength(0);
+
+ expect(() => bare.init("")).toThrow(/api key/i);
+ expect(() => bare.init(" ")).toThrow(/api key/i);
+ expect(() => bare.init("key", "http://evil.example/v2")).toThrow(/https/i);
+ expect(fake.records).toHaveLength(0);
+
+ const sdk = client("parity-key");
+ await expectRejected(sdk.validateEmail(""));
+ await expectRejected(sdk.validateEmail("a@example.com", { timeout: 0 }));
+ await expectRejected(sdk.validateEmail("a@example.com", { timeout: 2 }));
+ await expectRejected(sdk.validateEmail("a@example.com", { timeout: 61 }));
+ await expectRejected(sdk.validateBatch(null));
+ await expectRejected(sdk.validateBatch([{ email_address: "a@example.com" }], { timeout: 0 }));
+ await expectRejected(sdk.validateBatch([{ email_address: "a@example.com" }], { timeout: 9 }));
+ await expectRejected(sdk.getApiUsage("", "2020-01-31"));
+ await expectRejected(sdk.scoreEmail(""));
+ await expectRejected(sdk.sendFile({ email_address_column: 1 }));
+ await expectRejected(sdk.sendFile({ file }));
+ await expectRejected(sdk.findEmailByDomain({ first_name: "Ada" }));
+ await expectRejected(sdk.findEmailByDomain({ domain: "example.com" }));
+ await expectRejected(sdk.addFilter({ rule: "", target: "domain", value: "spam.com" }));
+ await expectRejected(sdk.getFile(""));
+ await expectRejected(sdk.exportS3file({ file_id: "id" }));
+ expect(fake.records).toHaveLength(0);
+ });
+
+ it("turns a 403 HTML access-denied page into a short error", async () => {
+ const error = await expectRejected(client("html-403-key").getCredits());
+ expect(error.message).toContain("Access denied");
+ expect(HTML_PAGE.length).toBeGreaterThan(100000);
+ });
+
+ it("turns a 403 HTML getFile page into a short error instead of the page", async () => {
+ const error = await expectRejected(client("html-403-file").getFile("file-1"));
+ expect(error.message).toContain("Access denied");
+ });
+
+ it("rejects HTML with status 200 for JSON and file downloads", async () => {
+ const credits = await expectRejected(client("html-200-credits").getCredits());
+ expect(credits.message).toContain("Access denied");
+ const fileError = await expectRejected(client("html-200-file").getFile("file-1"));
+ expect(fileError.message).toContain("Access denied");
+ const finder = await expectRejected(client("html-200-finder").getEmailFinderFile("file-1"));
+ expect(finder.message).toContain("Access denied");
+ const domain = await expectRejected(client("html-200-domain").getDomainSearchFile("file-1"));
+ expect(domain.message).toContain("Access denied");
+ });
+
+ it("rejects empty and malformed bodies without the parser text", async () => {
+ const empty = await expectRejected(client("empty-key").getCredits());
+ expect(empty.message).toMatch(/Empty response/);
+ const malformed = await expectRejected(client("malformed-key").validateEmail("a@example.com"));
+ expect(malformed.message).toMatch(/not valid JSON/);
+ const fileEmpty = await expectRejected(client("empty-file").getFile("file-1"));
+ expect(fileEmpty.message).toMatch(/Empty response/);
+ });
+
+ it("strips a repeated Error prefix", async () => {
+ const error = await expectRejected(client("double-error-key").getCredits());
+ expect(error.message).toBe("api_key is invalid");
+ });
+
+ it("uses the Cloudflare JSON title for a 403 access-denied payload", async () => {
+ const error = await expectRejected(client("cf-json-key").getCredits());
+ expect(error.message).toBe("Error 1020: Access denied");
+ });
+
+ it("returns a getFile JSON error object and still downloads CSV", async () => {
+ const previous = global.fetch;
+ const sdk = client("parity-key");
+ global.fetch = previous;
+ jest.spyOn(global, "fetch").mockImplementationOnce(() =>
+ Promise.resolve({
+ ok: true,
+ status: 200,
+ headers: { get: () => "application/json" },
+ text: () => Promise.resolve(JSON.stringify({ success: false, message: "File not ready" })),
+ })
+ );
+ await expect(sdk.getFile("file-1")).resolves.toEqual({ success: false, message: "File not ready" });
+ fake.restore();
+ fake = installFakeFetch();
+ const csvSdk = client("parity-key");
+ jest.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {});
+ await expect(csvSdk.getFile("file-1")).resolves.toBe("result.csv");
+ });
+
+ it("keeps API keys isolated across concurrent clients", async () => {
+ const clients = Array.from({ length: 12 }, (_, index) => {
+ const key = `client-${String(index).padStart(4, "0")}-secret`;
+ return { key, sdk: client(key) };
+ });
+ const results = await Promise.all(
+ clients.map(({ sdk }) => sdk.validateEmail(`person-${sdk._api_key}@example.com`))
+ );
+ expect(fake.records).toHaveLength(clients.length);
+ results.forEach((result, index) => {
+ expect(result.echo_api_key).toBe(clients[index].key);
+ const blob = JSON.stringify(result);
+ for (const other of clients) {
+ if (other.key !== clients[index].key) expect(blob).not.toContain(other.key);
+ }
+ });
+ for (const request of fake.records) {
+ expect(request.apiKey).toBe(request.fields.api_key);
+ expect(request.fields.email).toBe(`person-${request.apiKey}@example.com`);
+ for (const other of clients) {
+ if (other.key !== request.apiKey) {
+ expect(request.url).not.toContain(other.key);
+ expect(JSON.stringify(request.fields)).not.toContain(other.key);
+ }
+ }
+ }
+ });
+});
diff --git a/tests/ts-consumer.ts b/tests/ts-consumer.ts
index 0056a52..fc1da64 100644
--- a/tests/ts-consumer.ts
+++ b/tests/ts-consumer.ts
@@ -5,6 +5,7 @@
import ZeroBounceSDK, {
type ValidateEmailOptions,
+ type ValidateBatchOptions,
type ValidateBatchEmailItem,
type SendFileOptions,
type SendFileStreamOptions,
@@ -21,6 +22,9 @@ import ZeroBounceSDK, {
type ValidateBatchResponse,
type ApiUsageResponse,
type EmailActivityResponse,
+ type EmailScoringResult,
+ type EmailScoringError,
+ type ScoreEmailOptions,
type SendFileResponse,
type ValidationFileStatusResponse,
type ScoringFileStatusResponse,
@@ -46,6 +50,15 @@ const downloadCombined: string = ZeroBounceSDK.ZBDownloadType.COMBINED;
const validateOpts: ValidateEmailOptions = {
ip_address: "127.0.0.1",
timeout: 10,
+ activityData: true,
+ verifyPlus: false,
+ method: "POST",
+};
+
+const batchOpts: ValidateBatchOptions = {
+ timeout: 30,
+ activityData: true,
+ verifyPlus: true,
};
const batchItems: ValidateBatchEmailItem[] = [
@@ -82,11 +95,17 @@ const usagePromise: Promise =
zb.getApiUsage("2024-01-01", "2024-12-31");
const batchPromise: Promise =
- zb.validateBatch(batchItems);
+ zb.validateBatch(batchItems, batchOpts);
const activityPromise: Promise =
zb.getEmailActivity("test@example.com");
+const scoreOpts: ScoreEmailOptions = { method: "POST" };
+const scorePromise: Promise =
+ zb.scoreEmail("test@example.com");
+const scorePostPromise: Promise =
+ zb.scoreEmail("test@example.com", scoreOpts);
+
const findDomainPromise: Promise =
zb.findEmailByDomain(findByDomainOpts);
const findCompanyPromise: Promise =
@@ -198,6 +217,8 @@ function assertTypes(): void {
void usagePromise;
void batchPromise;
void activityPromise;
+ void scorePromise;
+ void scorePostPromise;
void findDomainPromise;
void findCompanyPromise;
void formatDomainPromise;
diff --git a/tests/zero-bounce.test.js b/tests/zero-bounce.test.js
index c041cb4..ad82770 100644
--- a/tests/zero-bounce.test.js
+++ b/tests/zero-bounce.test.js
@@ -48,8 +48,7 @@ describe("ZeroBounceSDK", () => {
describe("getCredits", () => {
it("should throw an error if not initialized", async () => {
- await zeroBounceSDK.getCredits();
- expect(console.error).toHaveBeenCalledWith(initErrorMessage);
+ await expect(zeroBounceSDK.getCredits()).rejects.toThrow(initErrorMessage);
});
it("should return error response with invalid API key", async () => {
@@ -83,24 +82,17 @@ describe("ZeroBounceSDK", () => {
const endDate = "2023-12-12";
it("should throw an error if not initialized", async () => {
- await zeroBounceSDK.getApiUsage(startDate, endDate);
- expect(console.error).toHaveBeenCalledWith(initErrorMessage);
+ await expect(zeroBounceSDK.getApiUsage(startDate, endDate)).rejects.toThrow(initErrorMessage);
});
it("should throw an error if stardDate is missing", async () => {
zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
- await zeroBounceSDK.getApiUsage(null, endDate);
- expect(console.error).toHaveBeenCalledWith(
- expect.stringContaining(missingParamMessage)
- );
+ await expect(zeroBounceSDK.getApiUsage(null, endDate)).rejects.toThrow(missingParamMessage)
});
it("should throw an error if endDate is missing", async () => {
zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
- await zeroBounceSDK.getApiUsage(startDate, null);
- expect(console.error).toHaveBeenCalledWith(
- expect.stringContaining(missingParamMessage)
- );
+ await expect(zeroBounceSDK.getApiUsage(startDate, null)).rejects.toThrow(missingParamMessage)
});
it("should return error response with invalid API key", async () => {
@@ -170,16 +162,12 @@ describe("ZeroBounceSDK", () => {
const ip_address = "127.0.0.1";
it("should throw an error if not initialized", async () => {
- await zeroBounceSDK.validateEmail(email, ip_address);
- expect(console.error).toHaveBeenCalledWith(initErrorMessage);
+ await expect(zeroBounceSDK.validateEmail(email, ip_address)).rejects.toThrow(initErrorMessage);
});
it("should throw an error if email is missing", async () => {
zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
- await zeroBounceSDK.validateEmail(null);
- expect(console.error).toHaveBeenCalledWith(
- expect.stringContaining(missingParamMessage)
- );
+ await expect(zeroBounceSDK.validateEmail(null)).rejects.toThrow(missingParamMessage)
});
it("should return error response with invalid API key", async () => {
@@ -290,18 +278,109 @@ describe("ZeroBounceSDK", () => {
it("should throw an error if timeout is less than 3", async () => {
zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
- await zeroBounceSDK.validateEmail(email, { timeout: 2 });
- expect(console.error).toHaveBeenCalledWith(
- expect.stringContaining("timeout parameter is invalid")
- );
+ await expect(zeroBounceSDK.validateEmail(email, { timeout: 2 })).rejects.toThrow("timeout parameter is invalid")
});
it("should throw an error if timeout is greater than 60", async () => {
zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
- await zeroBounceSDK.validateEmail(email, { timeout: 61 });
- expect(console.error).toHaveBeenCalledWith(
- expect.stringContaining("timeout parameter is invalid")
- );
+ await expect(zeroBounceSDK.validateEmail(email, { timeout: 61 })).rejects.toThrow("timeout parameter is invalid")
+ });
+
+ it("should pass activity_data and verify_plus via options object", async () => {
+ jest.spyOn(global, "fetch").mockImplementationOnce(() => Promise.resolve({
+ json: () => Promise.resolve({ status: "valid" }),
+ text: () => Promise.resolve('{"status":"valid"}'),
+ }));
+ zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
+ await zeroBounceSDK.validateEmail(email, {
+ activityData: true,
+ verifyPlus: false,
+ });
+ const calledUrl = String(fetch.mock.calls[0][0]);
+ expect(calledUrl).toContain("activity_data=true");
+ expect(calledUrl).toContain("verify_plus=false");
+ });
+
+ it("should send POST with form body when method is POST", async () => {
+ const expectedResponse = { status: "valid", address: email };
+
+ jest.spyOn(global, "fetch").mockImplementationOnce((url, init) => {
+ expect(String(url)).toContain("/validate");
+ expect(String(url)).not.toContain("api_key=");
+ expect(init.method).toBe("POST");
+ expect(init.body).toBeInstanceOf(URLSearchParams);
+ expect(init.body.get("email")).toBe(email);
+ expect(init.body.get("api_key")).toBe("valid-api-key");
+ expect(init.body.get("ip_address")).toBe(ip_address);
+ return Promise.resolve({
+ json: () => Promise.resolve(expectedResponse),
+ text: () => Promise.resolve(JSON.stringify(expectedResponse)),
+ });
+ });
+
+ zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
+ const response = await zeroBounceSDK.validateEmail(email, {
+ ip_address,
+ method: "POST",
+ });
+ expect(response).toEqual(expectedResponse);
+ });
+ });
+
+ describe("scoreEmail", () => {
+ const email = "mark@gmail.com";
+
+ it("should throw an error if not initialized", async () => {
+ await expect(zeroBounceSDK.scoreEmail(email)).rejects.toThrow(initErrorMessage);
+ });
+
+ it("should throw an error if email is missing", async () => {
+ zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
+ await expect(zeroBounceSDK.scoreEmail(null)).rejects.toThrow(missingParamMessage)
+ });
+
+ it("should return scoring data via GET", async () => {
+ const expectedResponse = {
+ email: "mark@gmail.com",
+ score: 10,
+ };
+
+ jest.spyOn(global, "fetch").mockImplementationOnce((url, init) => {
+ expect(String(url)).toContain("/scoring?");
+ expect(init.method).toBe("GET");
+ return Promise.resolve({
+ json: () => Promise.resolve(expectedResponse),
+ text: () => Promise.resolve(JSON.stringify(expectedResponse)),
+ });
+ });
+
+ zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
+ const response = await zeroBounceSDK.scoreEmail(email);
+ expect(response).toEqual(expectedResponse);
+ });
+
+ it("should send POST with form body when requested", async () => {
+ const expectedResponse = {
+ email: "mark@gmail.com",
+ score: 10,
+ };
+
+ jest.spyOn(global, "fetch").mockImplementationOnce((url, init) => {
+ expect(String(url)).toContain("/scoring");
+ expect(String(url)).not.toContain("api_key=");
+ expect(init.method).toBe("POST");
+ expect(init.body).toBeInstanceOf(URLSearchParams);
+ expect(init.body.get("email")).toBe(email);
+ expect(init.body.get("api_key")).toBe("valid-api-key");
+ return Promise.resolve({
+ json: () => Promise.resolve(expectedResponse),
+ text: () => Promise.resolve(JSON.stringify(expectedResponse)),
+ });
+ });
+
+ zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
+ const response = await zeroBounceSDK.scoreEmail(email, { method: "POST" });
+ expect(response).toEqual(expectedResponse);
});
});
@@ -313,25 +392,32 @@ describe("ZeroBounceSDK", () => {
];
it("should throw an error if not initialized", async () => {
- await zeroBounceSDK.validateBatch(emailBatch);
- expect(console.error).toHaveBeenCalledWith(initErrorMessage);
+ await expect(zeroBounceSDK.validateBatch(emailBatch)).rejects.toThrow(initErrorMessage);
});
it("should throw an error if email list is missing", async () => {
zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
- await zeroBounceSDK.validateBatch(null);
- expect(console.error).toHaveBeenCalledWith(
- expect.stringContaining(missingParamMessage)
- );
+ await expect(zeroBounceSDK.validateBatch(null)).rejects.toThrow(missingParamMessage)
});
it("should return error response with invalid API key", async () => {
+ const body = {
+ email_batch: [],
+ errors: [{ email_address: "all", error: "Invalid API Key or your account ran out of credits" }],
+ };
+ jest.spyOn(global, "fetch").mockImplementationOnce(() =>
+ Promise.resolve({
+ status: 200,
+ ok: true,
+ headers: { get: () => "application/json" },
+ text: () => Promise.resolve(JSON.stringify(body)),
+ json: () => Promise.resolve(body),
+ })
+ );
zeroBounceSDK.init("invalid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
- try {
- await zeroBounceSDK.validateBatch(emailBatch);
- } catch (error) {
- expect(error.message).toEqual('TypeError: Network request failed');
- }
+ await expect(zeroBounceSDK.validateBatch(emailBatch)).rejects.toThrow(
+ "Invalid API Key or your account ran out of credits"
+ );
});
it("should return the validated emails data", async () => {
@@ -397,6 +483,29 @@ describe("ZeroBounceSDK", () => {
expect(String(fetch.mock.calls[0][0])).not.toContain("bulkapi");
expect(response).toEqual(expectedResponse);
});
+
+ it("should include batch options in POST body", async () => {
+ jest.spyOn(global, "fetch").mockImplementationOnce(() => Promise.resolve({
+ json: () => Promise.resolve({ email_batch: [], errors: [] }),
+ text: () => Promise.resolve('{"email_batch":[],"errors":[]}'),
+ }));
+ zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
+ await zeroBounceSDK.validateBatch(emailBatch, {
+ timeout: 60,
+ activityData: true,
+ verifyPlus: true,
+ });
+ const init = fetch.mock.calls[0][1];
+ const body = JSON.parse(init.body);
+ expect(body.timeout).toBe(60);
+ expect(body.activity_data).toBe(true);
+ expect(body.verify_plus).toBe(true);
+ });
+
+ it("should throw an error if batch timeout is out of range", async () => {
+ zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
+ await expect(zeroBounceSDK.validateBatch(emailBatch, { timeout: 9 })).rejects.toThrow("timeout parameter is invalid")
+ });
});
@@ -404,16 +513,12 @@ describe("ZeroBounceSDK", () => {
const email = "valid@example.com";
it("should throw an error if not initialized", async () => {
- await zeroBounceSDK.getEmailActivity(email);
- expect(console.error).toHaveBeenCalledWith(initErrorMessage);
+ await expect(zeroBounceSDK.getEmailActivity(email)).rejects.toThrow(initErrorMessage);
});
it("should throw an error if email is missing", async () => {
zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
- await zeroBounceSDK.getEmailActivity(null);
- expect(console.error).toHaveBeenCalledWith(
- expect.stringContaining(missingParamMessage)
- );
+ await expect(zeroBounceSDK.getEmailActivity(null)).rejects.toThrow(missingParamMessage)
});
it("should return error response with invalid API key", async () => {
@@ -453,8 +558,7 @@ describe("ZeroBounceSDK", () => {
};
it("should throw an error if not initialized", async () => {
- await zeroBounceSDK.sendFile(payload);
- expect(console.error).toHaveBeenCalledWith(initErrorMessage);
+ await expect(zeroBounceSDK.sendFile(payload)).rejects.toThrow(initErrorMessage);
});
it("should throw an error if file is missing", async () => {
@@ -463,10 +567,7 @@ describe("ZeroBounceSDK", () => {
};
zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
- await zeroBounceSDK.sendFile(payload);
- expect(console.error).toHaveBeenCalledWith(
- expect.stringContaining(missingParamMessage)
- );
+ await expect(zeroBounceSDK.sendFile(payload)).rejects.toThrow(missingParamMessage)
});
it("should throw an error if email address column is missing", async () => {
@@ -475,21 +576,26 @@ describe("ZeroBounceSDK", () => {
};
zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
- await zeroBounceSDK.sendFile(payload);
- expect(console.error).toHaveBeenCalledWith(
- expect.stringContaining(missingParamMessage)
- );
+ await expect(zeroBounceSDK.sendFile(payload)).rejects.toThrow(missingParamMessage)
});
it("should return error response with invalid API key", async () => {
+ const expected = {
+ success: "False",
+ message: ["api_key is invalid"],
+ };
+ jest.spyOn(global, "fetch").mockImplementationOnce(() =>
+ Promise.resolve({
+ status: 200,
+ ok: true,
+ headers: { get: () => "application/json" },
+ text: () => Promise.resolve(JSON.stringify(expected)),
+ json: () => Promise.resolve(expected),
+ })
+ );
zeroBounceSDK.init("invalid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
const response = await zeroBounceSDK.sendFile(payload);
- expect(response).toEqual({
- "success": "False",
- "message": [
- "api_key is invalid"
- ]
- });
+ expect(response).toEqual(expected);
});
it("should return the data regarding sent file", async () => {
@@ -536,8 +642,7 @@ describe("ZeroBounceSDK", () => {
};
it("should throw an error if not initialized", async () => {
- await zeroBounceSDK.sendScoringFile(payload);
- expect(console.error).toHaveBeenCalledWith(initErrorMessage);
+ await expect(zeroBounceSDK.sendScoringFile(payload)).rejects.toThrow(initErrorMessage);
});
it("should throw an error if file is missing", async () => {
@@ -546,10 +651,7 @@ describe("ZeroBounceSDK", () => {
};
zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
- await zeroBounceSDK.sendScoringFile(payload);
- expect(console.error).toHaveBeenCalledWith(
- expect.stringContaining(missingParamMessage)
- );
+ await expect(zeroBounceSDK.sendScoringFile(payload)).rejects.toThrow(missingParamMessage)
});
it("should throw an error if email address column is missing", async () => {
@@ -558,21 +660,26 @@ describe("ZeroBounceSDK", () => {
};
zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
- await zeroBounceSDK.sendScoringFile(payload);
- expect(console.error).toHaveBeenCalledWith(
- expect.stringContaining(missingParamMessage)
- );
+ await expect(zeroBounceSDK.sendScoringFile(payload)).rejects.toThrow(missingParamMessage)
});
it("should return error response with invalid API key", async () => {
+ const expected = {
+ success: "False",
+ message: ["api_key is invalid"],
+ };
+ jest.spyOn(global, "fetch").mockImplementationOnce(() =>
+ Promise.resolve({
+ status: 200,
+ ok: true,
+ headers: { get: () => "application/json" },
+ text: () => Promise.resolve(JSON.stringify(expected)),
+ json: () => Promise.resolve(expected),
+ })
+ );
zeroBounceSDK.init("invalid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
const response = await zeroBounceSDK.sendScoringFile(payload);
- expect(response).toEqual({
- "success": "False",
- "message": [
- "api_key is invalid"
- ]
- });
+ expect(response).toEqual(expected);
});
it("should return the data regarding sent scoring file", async () => {
@@ -599,16 +706,12 @@ describe("ZeroBounceSDK", () => {
const fileId = "aaaaaaaa-zzzz-xxxx-yyyy-5003727fffff";
it("should throw an error if not initialized", async () => {
- await zeroBounceSDK.getFileStatus(fileId);
- expect(console.error).toHaveBeenCalledWith(initErrorMessage);
+ await expect(zeroBounceSDK.getFileStatus(fileId)).rejects.toThrow(initErrorMessage);
});
it("should throw an error if file id is missing", async () => {
zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
- await zeroBounceSDK.getFileStatus(null);
- expect(console.error).toHaveBeenCalledWith(
- expect.stringContaining(missingParamMessage)
- );
+ await expect(zeroBounceSDK.getFileStatus(null)).rejects.toThrow(missingParamMessage)
});
it("should return error response with invalid API key", async () => {
@@ -649,16 +752,12 @@ describe("ZeroBounceSDK", () => {
const fileId = "aaaaaaaa-zzzz-xxxx-yyyy-5003727fffff";
it("should throw an error if not initialized", async () => {
- await zeroBounceSDK.getScoringFileStatus(fileId);
- expect(console.error).toHaveBeenCalledWith(initErrorMessage);
+ await expect(zeroBounceSDK.getScoringFileStatus(fileId)).rejects.toThrow(initErrorMessage);
});
it("should throw an error if file id is missing", async () => {
zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
- await zeroBounceSDK.getScoringFileStatus(null);
- expect(console.error).toHaveBeenCalledWith(
- expect.stringContaining(missingParamMessage)
- );
+ await expect(zeroBounceSDK.getScoringFileStatus(null)).rejects.toThrow(missingParamMessage)
});
it("should return error response with invalid API key", async () => {
@@ -702,16 +801,12 @@ describe("ZeroBounceSDK", () => {
});
it("should throw an error if not initialized", async () => {
- await zeroBounceSDK.getFile(fileId);
- expect(console.error).toHaveBeenCalledWith(initErrorMessage);
+ await expect(zeroBounceSDK.getFile(fileId)).rejects.toThrow(initErrorMessage);
});
it("should throw an error if file id is missing", async () => {
zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
- await zeroBounceSDK.getFile(null);
- expect(console.error).toHaveBeenCalledWith(
- expect.stringContaining(missingParamMessage)
- );
+ await expect(zeroBounceSDK.getFile(null)).rejects.toThrow(missingParamMessage)
});
it("should return error response with invalid API key", async () => {
@@ -806,16 +901,12 @@ describe("ZeroBounceSDK", () => {
const fileId = "aaaaaaaa-zzzz-xxxx-yyyy-5003727fffff";
it("should throw an error if not initialized", async () => {
- await zeroBounceSDK.getScoringFile(fileId);
- expect(console.error).toHaveBeenCalledWith(initErrorMessage);
+ await expect(zeroBounceSDK.getScoringFile(fileId)).rejects.toThrow(initErrorMessage);
});
it("should throw an error if file id is missing", async () => {
zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
- await zeroBounceSDK.getScoringFile(null);
- expect(console.error).toHaveBeenCalledWith(
- expect.stringContaining(missingParamMessage)
- );
+ await expect(zeroBounceSDK.getScoringFile(null)).rejects.toThrow(missingParamMessage)
});
it("should return error response with invalid API key", async () => {
@@ -846,16 +937,12 @@ describe("ZeroBounceSDK", () => {
const fileId = "aaaaaaaa-zzzz-xxxx-yyyy-5003727fffff";
it("should throw an error if not initialized", async () => {
- await zeroBounceSDK.deleteFile(fileId);
- expect(console.error).toHaveBeenCalledWith(initErrorMessage);
+ await expect(zeroBounceSDK.deleteFile(fileId)).rejects.toThrow(initErrorMessage);
});
it("should throw an error if file id is missing", async () => {
zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
- await zeroBounceSDK.deleteFile(null);
- expect(console.error).toHaveBeenCalledWith(
- expect.stringContaining(missingParamMessage)
- );
+ await expect(zeroBounceSDK.deleteFile(null)).rejects.toThrow(missingParamMessage)
});
it("should return error response with invalid API key", async () => {
@@ -891,16 +978,12 @@ describe("ZeroBounceSDK", () => {
const fileId = "aaaaaaaa-zzzz-xxxx-yyyy-5003727fffff";
it("should throw an error if not initialized", async () => {
- await zeroBounceSDK.deleteScoringFile(fileId);
- expect(console.error).toHaveBeenCalledWith(initErrorMessage);
+ await expect(zeroBounceSDK.deleteScoringFile(fileId)).rejects.toThrow(initErrorMessage);
});
it("should throw an error if file id is missing", async () => {
zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
- await zeroBounceSDK.deleteScoringFile(null);
- expect(console.error).toHaveBeenCalledWith(
- expect.stringContaining(missingParamMessage)
- );
+ await expect(zeroBounceSDK.deleteScoringFile(null)).rejects.toThrow(missingParamMessage)
});
it("should return error response with invalid API key", async () => {
@@ -940,16 +1023,12 @@ describe("ZeroBounceSDK", () => {
}
it("should throw an error if not initialized", async () => {
- await zeroBounceSDK.findEmailByDomain(payload);
- expect(console.error).toHaveBeenCalledWith(initErrorMessage);
+ await expect(zeroBounceSDK.findEmailByDomain(payload)).rejects.toThrow(initErrorMessage);
});
it("should throw an error if domain is missing", async () => {
zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
- await zeroBounceSDK.findEmailByDomain({domain: null});
- expect(console.error).toHaveBeenCalledWith(
- expect.stringContaining(missingParamMessage)
- );
+ await expect(zeroBounceSDK.findEmailByDomain({domain: null})).rejects.toThrow(missingParamMessage)
});
it("should return error response with invalid API key", async () => {
@@ -991,16 +1070,12 @@ describe("ZeroBounceSDK", () => {
}
it("should throw an error if not initialized", async () => {
- await zeroBounceSDK.findEmailFormatByDomain(payload);
- expect(console.error).toHaveBeenCalledWith(initErrorMessage);
+ await expect(zeroBounceSDK.findEmailFormatByDomain(payload)).rejects.toThrow(initErrorMessage);
});
it("should throw an error if domain is missing", async () => {
zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
- await zeroBounceSDK.findEmailFormatByDomain({domain: null});
- expect(console.error).toHaveBeenCalledWith(
- expect.stringContaining(missingParamMessage)
- );
+ await expect(zeroBounceSDK.findEmailFormatByDomain({domain: null})).rejects.toThrow(missingParamMessage)
});
it("should return error response with invalid API key", async () => {
@@ -1033,4 +1108,128 @@ describe("ZeroBounceSDK", () => {
expect(response).toEqual(expectedResponse);
});
});
+
+ describe("Priority 3 bulk APIs", () => {
+ class File {}
+ const file = new File();
+
+ const jsonOk = (data) =>
+ Promise.resolve({
+ json: () => Promise.resolve(data),
+ text: () => Promise.resolve(JSON.stringify(data)),
+ ok: true,
+ headers: { get: () => "application/json" },
+ });
+
+ beforeEach(() => {
+ zeroBounceSDK.init("valid-api-key", ZeroBounceSDK.ApiURL.DEFAULT_API_URL);
+ });
+
+ it("sendEmailFinderFile targets bulkapi email-finder sendfile", async () => {
+ const fetch = jest.spyOn(global, "fetch").mockImplementationOnce(() =>
+ jsonOk({ success: true, file_id: "fid" })
+ );
+ await zeroBounceSDK.sendEmailFinderFile({ file, domain_column: 1 });
+ expect(String(fetch.mock.calls[0][0])).toContain(
+ "https://bulkapi.zerobounce.net/email-finder/sendfile"
+ );
+ });
+
+ it("getDomainSearchFileStatus uses domain-search filestatus on bulk root", async () => {
+ const fetch = jest.spyOn(global, "fetch").mockImplementationOnce(() =>
+ jsonOk({ success: true, file_status: "Complete" })
+ );
+ await zeroBounceSDK.getDomainSearchFileStatus("file-123");
+ expect(String(fetch.mock.calls[0][0])).toContain(
+ "https://bulkapi.zerobounce.net/domain-search/filestatus"
+ );
+ expect(String(fetch.mock.calls[0][0])).toContain("file_id=file-123");
+ });
+
+ it("sendListEvaluatorFile posts to v2 listevaluator", async () => {
+ const fetch = jest.spyOn(global, "fetch").mockImplementationOnce(() =>
+ jsonOk({ file_id: "le-1" })
+ );
+ await zeroBounceSDK.sendListEvaluatorFile({ file, email_address_column: 2 });
+ expect(String(fetch.mock.calls[0][0])).toBe(
+ "https://bulkapi.zerobounce.net/v2/listevaluator/"
+ );
+ });
+
+ it("getListEvaluatorResult GETs file id path on bulk v2", async () => {
+ const fetch = jest.spyOn(global, "fetch").mockImplementationOnce(() =>
+ jsonOk({ file_status: "Complete" })
+ );
+ await zeroBounceSDK.getListEvaluatorResult("abc-def");
+ expect(String(fetch.mock.calls[0][0])).toContain(
+ "https://bulkapi.zerobounce.net/v2/listevaluator/abc-def/"
+ );
+ });
+
+ it("addFilter POSTs to main API filters/add", async () => {
+ const fetch = jest.spyOn(global, "fetch").mockImplementationOnce(() =>
+ jsonOk({ Message: "Filter successfully added" })
+ );
+ await zeroBounceSDK.addFilter({
+ rule: "allow",
+ target: "domain",
+ value: "example.com",
+ });
+ const [url, init] = fetch.mock.calls[0];
+ expect(String(url)).toContain("https://api.zerobounce.net/v2/filters/add");
+ expect(init.method).toBe("POST");
+ });
+
+ it("listFilters supports POST method option", async () => {
+ const fetch = jest.spyOn(global, "fetch").mockImplementationOnce(() =>
+ jsonOk([])
+ );
+ await zeroBounceSDK.listFilters({ method: "POST" });
+ const [url, init] = fetch.mock.calls[0];
+ expect(String(url)).toContain("/filters/list");
+ expect(init.method).toBe("POST");
+ });
+
+ it("listS3files GETs bulkapi listS3files with optional bucket and search", async () => {
+ const fetch = jest.spyOn(global, "fetch").mockImplementationOnce(() =>
+ jsonOk([{ name: "uploads/list.csv", size: 100, last_modified: "2024-01-01T00:00:00Z" }])
+ );
+ await zeroBounceSDK.listS3files({
+ bucket: "my-bucket",
+ search: "list",
+ });
+ const calledUrl = String(fetch.mock.calls[0][0]);
+ expect(calledUrl).toBe(
+ "https://bulkapi.zerobounce.net/listS3files?api_key=valid-api-key&bucket=my-bucket&search=list"
+ );
+ expect(fetch.mock.calls[0][1].method).toBe("GET");
+ });
+
+ it("validateS3file posts to bulkapi validateS3file", async () => {
+ const fetch = jest.spyOn(global, "fetch").mockImplementationOnce(() =>
+ jsonOk({ success: true, file_id: "s3-job" })
+ );
+ await zeroBounceSDK.validateS3file({
+ file_name: "emails/list.csv",
+ has_header_row: true,
+ remove_duplicates: true,
+ });
+ expect(String(fetch.mock.calls[0][0])).toBe(
+ "https://bulkapi.zerobounce.net/validateS3file"
+ );
+ });
+
+ it("exportS3file posts file_id and export_bucket", async () => {
+ const fetch = jest.spyOn(global, "fetch").mockImplementationOnce(() =>
+ jsonOk({ success: true })
+ );
+ await zeroBounceSDK.exportS3file({
+ file_id: "job-1",
+ export_bucket: "my-bucket",
+ });
+ const body = fetch.mock.calls[0][1].body;
+ expect(body.get("file_id")).toBe("job-1");
+ expect(body.get("export_bucket")).toBe("my-bucket");
+ });
+ });
});
\ No newline at end of file
diff --git a/zeroBounceSDK.d.ts b/zeroBounceSDK.d.ts
index 3432372..3499fe3 100644
--- a/zeroBounceSDK.d.ts
+++ b/zeroBounceSDK.d.ts
@@ -69,6 +69,10 @@ export interface EmailValidationResult {
account?: string | null;
domain?: string | null;
domain_age_days?: string | null;
+ /** Last activity bucket when activity_data was requested (e.g. 30, 365+). */
+ active_in_days?: string | null;
+ /** First date activity was detected for this email (YYYY-MM-DD). */
+ active_first_seen?: string | null;
smtp_provider?: string | null;
/** [true/false or null] Is the domain a catch-all? */
catchall_domain?: boolean | null;
@@ -138,6 +142,23 @@ export interface EmailActivityResponse {
active_in_days?: string;
}
+/** Successful GET/POST /scoring response. */
+export interface EmailScoringResult {
+ email: string;
+ score: number;
+}
+
+/** Unsuccessful /scoring response (invalid email, missing params, etc.). */
+export interface EmailScoringError {
+ error: string;
+}
+
+/** Options for scoreEmail. */
+export interface ScoreEmailOptions {
+ /** When `"POST"`, sends `api_key` and `email` as `application/x-www-form-urlencoded` (default GET). */
+ method?: "GET" | "POST";
+}
+
/** Bulk sendfile success (validation or scoring). */
export interface BulkSendFileSuccessResponse {
success: true;
@@ -231,11 +252,28 @@ export interface ValidateEmailOptions {
ip_address?: string;
/** Validation timeout in seconds, 3–60. If exceeded, API may return unknown/greylisted. */
timeout?: number;
+ /** Maps to `activity_data` on GET /validate. */
+ activityData?: boolean;
+ /** Maps to `verify_plus` on GET /validate. */
+ verifyPlus?: boolean;
+ /** Use form-encoded POST instead of GET (same parameters). */
+ method?: "GET" | "POST";
}
/** Single item for validateBatch (email_batch array). */
export interface ValidateBatchEmailItem {
email_address: string;
+ ip_address?: string | null;
+}
+
+/** Options for validateBatch (timeout, activity_data, verify_plus). */
+export interface ValidateBatchOptions {
+ /** Validation timeout in seconds, 10–120 for the whole batch. */
+ timeout?: number;
+ /** Maps to `activity_data` on POST /validatebatch. */
+ activityData?: boolean;
+ /** Maps to `verify_plus` on POST /validatebatch. */
+ verifyPlus?: boolean;
}
/** Bulk `getfile` query values for `download_type` (validation and scoring). */
@@ -310,6 +348,134 @@ export interface SendScoringFileStreamOptions {
remove_duplicate?: boolean;
}
+/** Options for bulk email finder sendfile. */
+export interface SendEmailFinderFileOptions {
+ file: File;
+ domain_column: number;
+ first_name_column?: number | false;
+ last_name_column?: number | false;
+ middle_name_column?: number | false;
+ full_name_column?: number | false;
+ has_header_row?: boolean;
+}
+
+export interface SendEmailFinderFileStreamOptions {
+ domain_column: number;
+ first_name_column?: number | false;
+ last_name_column?: number | false;
+ middle_name_column?: number | false;
+ full_name_column?: number | false;
+ has_header_row?: boolean;
+}
+
+/** Options for bulk domain search sendfile. */
+export interface SendDomainSearchFileOptions {
+ file: File;
+ domain_column: number;
+ has_header_row?: boolean;
+}
+
+export interface SendDomainSearchFileStreamOptions {
+ domain_column: number;
+ has_header_row?: boolean;
+}
+
+/** POST /v2/listevaluator/ success (201). */
+export interface ListEvaluatorSubmitResponse {
+ file_id?: string;
+ message?: string;
+ success?: boolean;
+}
+
+/** GET /v2/listevaluator/{file_id}/ while processing or complete. */
+export interface ListEvaluatorResultResponse {
+ file_status?: string;
+ success?: boolean;
+ message?: string;
+ [key: string]: unknown;
+}
+
+export interface SendListEvaluatorFileOptions {
+ file: File;
+ email_address_column: number;
+}
+
+export interface SendListEvaluatorFileStreamOptions {
+ email_address_column: number;
+}
+
+export interface SendEvaluatedFileOptions {
+ file: File;
+}
+
+export type FilterRule = "allow" | "block";
+export type FilterTarget = "email" | "domain" | "mx" | "tld";
+
+export interface FilterRuleParams {
+ rule: FilterRule;
+ target: FilterTarget;
+ value: string;
+}
+
+export interface FilterListItem {
+ rule: FilterRule;
+ target: FilterTarget;
+ value: string;
+}
+
+export interface FilterMessageResponse {
+ Message?: string;
+ Error?: string;
+}
+
+export interface ListFiltersOptions {
+ /** When `"POST"`, uses form-encoded POST (supported from Sep 2026). */
+ method?: "GET" | "POST";
+}
+
+export interface ValidateS3FileOptions {
+ file_name: string;
+ has_header_row?: boolean;
+ /** Postman field name for duplicate removal. */
+ remove_duplicates?: boolean;
+ /** Alternate field name used by some bulk endpoints. */
+ remove_duplicate?: boolean;
+ email_address_column?: number;
+ return_url?: string | false;
+ first_name_column?: number | false;
+ last_name_column?: number | false;
+ gender_column?: number | false;
+ ip_address_column?: number | false;
+ allowPhase2?: boolean | null;
+}
+
+export interface ScoreS3FileOptions {
+ file_name: string;
+ has_header_row?: boolean;
+ remove_duplicates?: boolean;
+ remove_duplicate?: boolean;
+ email_address_column?: number;
+}
+
+export interface ExportS3FileOptions {
+ file_id: string;
+ export_bucket: string;
+}
+
+/** Single entry from GET /listS3files. */
+export interface S3ListFileEntry {
+ name: string;
+ size: number;
+ last_modified: string;
+}
+
+export interface S3BulkJobResponse {
+ success?: boolean;
+ message?: string;
+ file_id?: string;
+ export_path?: string;
+}
+
/** Options for findEmailByDomain / Email Finder. */
export interface FindEmailByDomainOptions {
domain: string;
@@ -365,7 +531,7 @@ export class ZeroBounceSDK {
/**
* Validate a single email address.
* @param email - Email to validate
- * @param options - Optional ip_address and/or timeout (3–60 seconds), or ip_address string for backwards compatibility
+ * @param options - Optional ip_address, timeout (3–60), activityData, verifyPlus, or ip_address string for backwards compatibility
*/
validateEmail(
email: string,
@@ -384,10 +550,12 @@ export class ZeroBounceSDK {
/**
* Validate a batch of emails.
- * @param emailBatch - Array of objects with email_address
+ * @param emailBatch - Array of objects with email_address (and optional ip_address)
+ * @param options - Optional timeout (10–120), activityData, verifyPlus
*/
validateBatch(
- emailBatch: ValidateBatchEmailItem[]
+ emailBatch: ValidateBatchEmailItem[],
+ options?: ValidateBatchOptions | null
): Promise;
/**
@@ -396,6 +564,16 @@ export class ZeroBounceSDK {
*/
getEmailActivity(email: string): Promise;
+ /**
+ * Real-time AI scoring for a single email (GET or POST /scoring).
+ * @param email - Email to score
+ * @param options - Optional `{ method: "POST" }` for form-encoded POST
+ */
+ scoreEmail(
+ email: string,
+ options?: ScoreEmailOptions | null
+ ): Promise;
+
/**
* Submit a file for bulk email validation.
*/
@@ -484,6 +662,96 @@ export class ZeroBounceSDK {
fileId: string
): Promise;
+ sendEmailFinderFile(
+ options: SendEmailFinderFileOptions
+ ): Promise;
+
+ sendEmailFinderFileStream(
+ fileStreamOrBlob: File | Blob,
+ fileName: string,
+ options?: SendEmailFinderFileStreamOptions
+ ): Promise;
+
+ getEmailFinderFileStatus(
+ fileId: string
+ ): Promise;
+
+ getEmailFinderFile(
+ fileId: string
+ ): Promise;
+
+ deleteEmailFinderFile(
+ fileId: string
+ ): Promise;
+
+ sendDomainSearchFile(
+ options: SendDomainSearchFileOptions
+ ): Promise;
+
+ sendDomainSearchFileStream(
+ fileStreamOrBlob: File | Blob,
+ fileName: string,
+ options?: SendDomainSearchFileStreamOptions
+ ): Promise;
+
+ getDomainSearchFileStatus(
+ fileId: string
+ ): Promise;
+
+ getDomainSearchFile(
+ fileId: string
+ ): Promise;
+
+ deleteDomainSearchFile(
+ fileId: string
+ ): Promise;
+
+ sendListEvaluatorFile(
+ options: SendListEvaluatorFileOptions
+ ): Promise;
+
+ sendListEvaluatorFileStream(
+ fileStreamOrBlob: File | Blob,
+ fileName: string,
+ options?: SendListEvaluatorFileStreamOptions
+ ): Promise;
+
+ getListEvaluatorResult(
+ fileId: string
+ ): Promise;
+
+ sendEvaluatedFile(
+ options: SendEvaluatedFileOptions
+ ): Promise;
+
+ addFilter(
+ options: FilterRuleParams
+ ): Promise;
+
+ listFilters(
+ options?: ListFiltersOptions | null
+ ): Promise;
+
+ deleteFilter(
+ options: FilterRuleParams
+ ): Promise;
+
+ listS3files(
+ options?: { bucket?: string | null; search?: string | null } | null
+ ): Promise;
+
+ validateS3file(
+ options: ValidateS3FileOptions
+ ): Promise;
+
+ scoreS3file(
+ options: ScoreS3FileOptions
+ ): Promise;
+
+ exportS3file(
+ options: ExportS3FileOptions
+ ): Promise;
+
/**
* Email Finder: find email by domain and name.
*/