Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,16 @@ There is no separate lint command. CI (`.github/workflows/sdk_ci.yml`) runs `npm

## Conventions

- **Exactly the documented set of fields, no more and no fewer.** The eight S3 / SFTP file jobs go through `_bulkRootFileJob` and send `file_name`, `has_header_row`, `remove_duplicates` and `return_url` only. The live API requires `file_name` and `return_url` on every one of them, including `validateSFTPfile`, although the docs mark them optional.
- **Only endpoints documented at https://www.zerobounce.net/docs/.** Do not add a method for an endpoint that the public docs do not describe, even if it exists on the server. Check before concluding something is undocumented: the S3 bulk endpoints live under https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api, not the main API reference.
- **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.
- **List evaluator and the Inbox Placement Tester have their own base URLs**, set on the instance by `init`'s third argument: `https://bulkapi.zerobounce.net/v2/listevaluator` and `https://bulkapi.zerobounce.net/api/ipt`. They are not built from the bulk base. `sendEvaluatedFile` stays on the bulk `/v2` base.
- **`allow_phase_2` is validation bulk only** (`sendFile`, `sendFileStream`), and only when `allowPhase2` is set. Scoring `sendFile` ignores it. The S3 and SFTP file jobs send only the documented set and so never send it. `activity_data` on get-file is validation `getFile` only.
- **List evaluator, the Inbox Placement Tester, the bulk email finder and bulk domain search have their own base URLs**, set on the instance by `init`'s third argument: `https://bulkapi.zerobounce.net/v2/listevaluator`, `https://bulkapi.zerobounce.net/api/ipt`, `https://bulkapi.zerobounce.net/email-finder` and `https://bulkapi.zerobounce.net/domain-search`. They are not built from the bulk base, and each is passed to `createRequest` as `baseURL` with a path relative to it. `sendEvaluatedFile` stays on the bulk `/v2` base.
- **The Inbox Placement Tester takes the API key as the `api_key` query parameter and JSON bodies with camelCase fields.** Only fields the caller set are sent. Its error bodies use `detail`.
- **`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`.
- **`validateBatch` uses `api.zerobounce.net`**, not `bulkapi.zerobounce.net`, and sends its body as `jsonBody` so `createRequest` sets `Content-Type: application/json`. 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"`.
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- `init`'s `emailFinderBaseURL` and `domainSearchBaseURL` options are now honoured. They were validated and stored but never read, so the bulk email finder and bulk domain search always went to `https://bulkapi.zerobounce.net`. Both default to the same hosts as before, so a client that does not set them is unaffected.
- **BREAKING:** `sendEmailFinderFile` and `sendEmailFinderFileStream` require `first_name_column` or `full_name_column`, as the bulk email finder documents. As with every optional column, `0` and `false` mean "leave this column out" and so do not count as given.
- **BREAKING:** `validateS3file` and `scoreS3file` now send exactly the documented job fields — `file_name`, `has_header_row`, `remove_duplicates` and `return_url` — and go through the same path as their six siblings. They no longer send the undocumented `email_address_column`, `first_name_column`, `last_name_column`, `gender_column`, `ip_address_column`, `allow_phase_2` or singular `remove_duplicate`. `file_name`, `has_header_row` and `return_url` are now required on both: the live API rejects a job without `return_url` ("Provided return URL is not permitted: None"). `allowPhase2` remains available on `sendFile` and `sendFileStream`.
- `findEmailByDomain` and `findEmailByCompanyName` accept a missing `first_name` instead of rejecting it, as the Email Finder documents it as optional. Without it the API answers with the domain's or company's email format. `domain` or `company_name` is still required.
- `exportS3file` accepts a missing `export_bucket` instead of rejecting it, matching `exportSFTPfile`'s `export_path`. Without it the output bucket from your ZeroBounce settings is used.

### Fixed

- `validateBatch` sends its JSON body with `Content-Type: application/json`. It was handed to `fetch` as a plain string, which labelled the JSON `text/plain;charset=UTF-8`.
- Warmup `addSendingAccount` rejects `engagementRuleId` for a `zb_managed` account rather than silently dropping it. The field belongs to `self_managed` only.
- `validateSFTPfile` no longer passes a third argument that the shared job helper never had, and its documentation no longer says `file_name` is optional — the live API requires it.

## [3.0.0] - 2026-10-25

This release is a major version because the error contract changed: every public
Expand Down
71 changes: 62 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,27 +141,74 @@ try {
- ##### S3 and SFTP/FTP bulk jobs

```javascript
// All eight jobs take the same four fields and nothing else: file_name,
// has_header_row, remove_duplicates and return_url. The live API requires
// file_name and return_url on every one of them, including validateSFTPfile,
// although the docs mark them optional.

// Amazon S3: https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api
await zeroBounce.emailFinderS3file({ file_name: "lists/leads.csv", has_header_row: true });
await zeroBounce.validateS3file({
file_name: "lists/leads.csv",
has_header_row: true,
return_url: "https://example.com/zerobounce-done",
});
await zeroBounce.scoreS3file({
file_name: "lists/leads.csv",
has_header_row: true,
return_url: "https://example.com/zerobounce-done",
});
await zeroBounce.emailFinderS3file({
file_name: "lists/leads.csv",
has_header_row: true,
return_url: "https://example.com/zerobounce-done",
});
await zeroBounce.domainSearchS3file({
file_name: "lists/leads.csv",
has_header_row: true,
remove_duplicates: true,
return_url: "https://example.com/zerobounce-done",
});

// Without export_bucket the output bucket from your settings is used.
await zeroBounce.exportS3file({ file_id: "<FILE_ID>" });
await zeroBounce.exportS3file({ file_id: "<FILE_ID>", export_bucket: "my-bucket" });

// SFTP/FTP: https://www.zerobounce.net/docs/api-integrations/sftp_bulk_api
// The server, path and protocol come from your ZeroBounce settings.
// file_name is optional for validateSFTPfile only.
await zeroBounce.validateSFTPfile({ has_header_row: true });
await zeroBounce.scoreSFTPfile({ file_name: "my_file.txt", has_header_row: true });
await zeroBounce.emailFinderSFTPfile({ file_name: "my_file.txt", has_header_row: true });
await zeroBounce.domainSearchSFTPfile({ file_name: "my_file.txt", has_header_row: true });
const sftpJob = {
file_name: "my_file.txt",
has_header_row: true,
return_url: "https://example.com/zerobounce-done",
};
await zeroBounce.validateSFTPfile(sftpJob);
await zeroBounce.scoreSFTPfile(sftpJob);
await zeroBounce.emailFinderSFTPfile(sftpJob);
await zeroBounce.domainSearchSFTPfile(sftpJob);

// Without export_path the path from your settings is used.
await zeroBounce.exportSFTPfile({ file_id: "<FILE_ID>", export_path: "/exports" });
```

- ##### Bulk email finder

```javascript
// domain_column is required, and so is a name to work from: send either
// first_name_column or full_name_column. As with every optional column, 0 and
// false mean "leave this column out", so neither counts as a name column.
await zeroBounce.sendEmailFinderFile({
file: "<FILE>",
domain_column: 1,
first_name_column: 2,
last_name_column: 3,
has_header_row: true,
});
await zeroBounce.sendEmailFinderFile({
file: "<FILE>",
domain_column: 1,
full_name_column: 2,
});
```

- ##### Inbox Placement Tester

```javascript
Expand Down Expand Up @@ -199,11 +246,14 @@ try {
- ##### Point an API at a different base URL

```javascript
// The list evaluator and the Inbox Placement Tester have their own base URLs,
// separate from the main and bulk hosts.
// The list evaluator, the Inbox Placement Tester, the bulk email finder and
// bulk domain search each have their own base URL, separate from the main and
// bulk hosts. Every one of them must be https://.
zeroBounce.init("<YOUR_API_KEY>", ZeroBounceSDK.ApiURL.DEFAULT_API_URL, {
listEvaluatorBaseURL: "https://bulkapi.zerobounce.net/v2/listevaluator",
inboxPlacementBaseURL: "https://bulkapi.zerobounce.net/api/ipt",
emailFinderBaseURL: "https://bulkapi.zerobounce.net/email-finder",
domainSearchBaseURL: "https://bulkapi.zerobounce.net/domain-search",
});
```

Expand Down Expand Up @@ -422,8 +472,9 @@ try {
// The email domain for which to find the email format.
// company_name: String
// The company name for which to find the email format.
// first_name: String
// first_name: String or null (Optional)
// The first name of the person whose email format is being searched.
// Left out, the API answers with the domain's or company's email format.
// middle_name: String or null (Optional)
// The middle name of the person whose email format is being searched.
// last_name: String or null (Optional)
Expand All @@ -446,6 +497,8 @@ const companyNamePayload = {
try {
const domainResponse = await zeroBounce.findEmailByDomain(domainPayload);
const companyNameResponse = await zeroBounce.findEmailByCompanyName(companyNamePayload);
// first_name is optional: this asks for the domain's format on its own.
const formatOnly = await zeroBounce.findEmailByDomain({ domain: "<DOMAIN>" });
} catch (error) {
console.error(error);
}
Expand Down
55 changes: 48 additions & 7 deletions documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,27 +95,67 @@ try {
- ##### S3 and SFTP/FTP bulk jobs

```javascript
// All eight jobs take the same four fields and nothing else: file_name,
// has_header_row, remove_duplicates and return_url. The live API requires
// file_name and return_url on every one of them, including validateSFTPfile,
// although the docs mark them optional.

// Amazon S3: https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api
await zeroBounce.emailFinderS3file({ file_name: "lists/leads.csv", has_header_row: true });
await zeroBounce.validateS3file({
file_name: "lists/leads.csv",
has_header_row: true,
return_url: "https://example.com/zerobounce-done",
});
await zeroBounce.scoreS3file({
file_name: "lists/leads.csv",
has_header_row: true,
return_url: "https://example.com/zerobounce-done",
});
await zeroBounce.emailFinderS3file({
file_name: "lists/leads.csv",
has_header_row: true,
return_url: "https://example.com/zerobounce-done",
});
await zeroBounce.domainSearchS3file({
file_name: "lists/leads.csv",
has_header_row: true,
remove_duplicates: true,
return_url: "https://example.com/zerobounce-done",
});

// Without export_bucket the output bucket from your settings is used.
await zeroBounce.exportS3file({ file_id: "<FILE_ID>" });

// SFTP/FTP: https://www.zerobounce.net/docs/api-integrations/sftp_bulk_api
// The server, path and protocol come from your ZeroBounce settings.
// file_name is optional for validateSFTPfile only.
await zeroBounce.validateSFTPfile({ has_header_row: true });
await zeroBounce.scoreSFTPfile({ file_name: "my_file.txt", has_header_row: true });
await zeroBounce.emailFinderSFTPfile({ file_name: "my_file.txt", has_header_row: true });
await zeroBounce.domainSearchSFTPfile({ file_name: "my_file.txt", has_header_row: true });
const sftpJob = {
file_name: "my_file.txt",
has_header_row: true,
return_url: "https://example.com/zerobounce-done",
};
await zeroBounce.validateSFTPfile(sftpJob);
await zeroBounce.scoreSFTPfile(sftpJob);
await zeroBounce.emailFinderSFTPfile(sftpJob);
await zeroBounce.domainSearchSFTPfile(sftpJob);

// Without export_path the path from your settings is used.
await zeroBounce.exportSFTPfile({ file_id: "<FILE_ID>", export_path: "/exports" });
```

- ##### Bulk email finder

```javascript
// domain_column is required, and so is a name to work from: send either
// first_name_column or full_name_column. As with every optional column, 0 and
// false mean "leave this column out", so neither counts as a name column.
await zeroBounce.sendEmailFinderFile({
file: "<FILE>",
domain_column: 1,
first_name_column: 2,
has_header_row: true,
});
```

- ##### Inbox Placement Tester

```javascript
Expand Down Expand Up @@ -376,8 +416,9 @@ try {
// The email domain for which to find the email format.
// company_name: String
// The company name for which to find the email format.
// first_name: String
// first_name: String or null (Optional)
// The first name of the person whose email format is being searched.
// Left out, the API answers with the domain's or company's email format.
// middle_name: String or null (Optional)
// The middle name of the person whose email format is being searched.
// last_name: String or null (Optional)
Expand Down
7 changes: 5 additions & 2 deletions src/warmup.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ export class ZeroBounceWarmup {
*
* `self_managed` runs your own mailbox and needs `engagementRuleId` 0-4.
* `zb_managed` has ZeroBounce provision it and takes `emailContent` and
* `managedSending` instead; a 200 means accepted, and the mailbox is
* provisioned afterwards.
* `managedSending` instead, and rejects `engagementRuleId`; a 200 means
* accepted, and the mailbox is provisioned afterwards.
*/
addSendingAccount({
fromAddress,
Expand All @@ -204,6 +204,9 @@ export class ZeroBounceWarmup {
if (emailContent != null || managedSending != null) {
throw clientError("email_content and managed_sending are for zb_managed.");
}
} else if (engagementRuleId != null) {
// zb_managed has no engagement_rule_id field: say so rather than drop it.
throw clientError("engagement_rule_id is for self_managed.");
}
if (ignoreConflict != null) body.ignore_conflict = Boolean(ignoreConflict);
if (emailContent != null) {
Expand Down
Loading
Loading