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
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Guidance for AI coding agents working in this repository.

- `include/ZeroBounce/`: public headers. `ZeroBounce.h` is the client.
- `src/ZeroBounce.cpp`: validation, bulk file, credits, activity, scoring.
- `src/Priority3.cpp`: bulk email finder, domain search, list evaluator, filters, the S3 / SFTP bulk jobs, and the Inbox Placement Tester. The six S3 / SFTP file jobs build their form through `bulkFileJobFields`; two shapes differ from the rest and are easy to get wrong — `validateSFTPFile`'s `fileName` may be unset, and `exportSFTPFile` sends `export_path`, not `export_bucket`.
- `src/Priority3.cpp`: bulk email finder, domain search, list evaluator, filters, the S3 / SFTP bulk jobs, and the Inbox Placement Tester. All six S3 / SFTP file jobs build their shared form fields through `bulkFileJobFields`; `validateS3File` and `scoreS3File` then append the extra fields only they take. Do not re-implement those shared fields per job: that duplication is how `return_url` came to be optional on two of them. `exportSFTPFile` sends `export_path`, not the `export_bucket` `exportS3File` sends, and both are optional.
- `src/HttpError.cpp`: turns HTTP failures, HTML pages, and empty or non-JSON bodies into a short `ZBErrorResponse`.
- `test/ZeroBounceTest.cpp`: existing GoogleTest cases with a scripted response.
- `test/OfflineApiTest.cpp`: offline suite. A fake `BaseRequestHandler` records every request and answers without the network.
Expand Down Expand Up @@ -44,15 +44,15 @@ log, a fixture or a message.
## Conventions

- **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.
- **The list evaluator and the Inbox Placement Tester have their own base URLs** (`listEvaluatorBaseUrl`, `inboxPlacementBaseUrl`, settable via `setListEvaluatorBaseUrl` / `setInboxPlacementBaseUrl`). They are not built from the bulk base, and `sendEvaluatedFile` stays on it.
- **Where the live API and the docs disagree, follow the API and say so in a comment at the call site.** The ones found so far: `validatebatch` takes `timeout` as a JSON *string*; the Inbox Placement Tester authenticates with `Authorization: Bearer`, not `api_key`; its listing is `GET /tests`, not `/test`; `send-evaluated-file` takes `file_id` as a form field, not a file upload; the S3 and SFTP jobs require `return_url` and `file_name`, `validateSFTPFile` included; the Warmup `check-domain-dmarc` takes an email address, not a domain; and a failed SMTP login in `test-smtp-configuration` comes back as an HTTP 500.
- **The list evaluator and the Inbox Placement Tester have their own base URLs** (`listEvaluatorBaseUrl`, `inboxPlacementBaseUrl`, settable via `setListEvaluatorBaseUrl` / `setInboxPlacementBaseUrl`). They are not built from the bulk base, and `sendEvaluatedFile` stays on it. The three bulk hosts have setters too — `setBulkApiRootUrl`, `setBulkApiBaseUrl`, `setBulkApiScoringBaseUrl` — so every bulk endpoint can be pointed at a test host. Keep every base URL settable; a hardcoded `const` one makes its endpoints untestable.
- **Where the live API and the docs disagree, follow the API and say so in a comment at the call site.** The ones found so far: `validatebatch` takes `timeout` as a JSON *string*; the Inbox Placement Tester authenticates with `Authorization: Bearer`, not `api_key`; its listing is `GET /tests`, not `/test`; `send-evaluated-file` takes `file_id` as a form field, not a file upload; all six S3 and SFTP jobs require `return_url` and `file_name`, `validateS3File`, `scoreS3File` and `validateSFTPFile` included (`POST /validateS3file` without one answers 400 `Provided return URL is not permitted: None`); the Warmup `check-domain-dmarc` takes an email address, not a domain; and a failed SMTP login in `test-smtp-configuration` comes back as an HTTP 500.
- **The Inbox Placement Tester takes the API key as an `Authorization: Bearer` header and JSON bodies with camelCase fields.** Only fields the caller set are sent. Its error bodies use `detail`. It and the Warmup client are the only callers of `BaseRequestHandler::Put` and `Delete`, which have default implementations so existing subclasses keep compiling. The Warmup API's `DELETE /custom-engagement` carries a JSON body, which is why there is a `Delete` overload that takes one.
- **`{"success": false}` on an HTTP 200 is a failure.** The API answers an unknown bulk file id that way. `zbReadJsonResponse` turns it into an error callback.
- **`src/ZeroBounceWarmup.cpp` is a separate client for the Warmup API**, a different product with its own host and its own team-scoped key. It is not part of `ZeroBounce`, and it owns a `unique_ptr`, so like `ZeroBounce` it is not copyable.

- **One `ZeroBounce` per API key.** `getInstance()` is a process-wide singleton. Do not reuse it across users or requests. Construct a separate object per key. The offline concurrency test does this.
- **Column indexes are 1-based.** `SendFileOptions` name, gender, and IP columns default to `0`, and `0` means the field is left out. `ValidateS3FileOptions::emailAddressColumn` is `std::optional<int>`: when it is set, `0` is sent. `sendFile`'s email column is required and is sent as given, including `0`.
- **Optional parameters are omitted when unset.** `timeout`, `activityData`, `verifyPlus`, `allowPhase2`, `downloadType`, and `hasHeaderRow` (`std::optional<bool>`) are not sent until the caller sets them. Explicit `false` is sent as `false`.
- **Optional parameters are omitted when unset.** `timeout`, `activityData`, `verifyPlus`, `allowPhase2`, `downloadType`, and `hasHeaderRow` (`std::optional<bool>`) are not sent until the caller sets them. Explicit `false` is sent as `false`. The S3 / SFTP jobs are the exception: their `hasHeaderRow` is a plain `bool` defaulting to `true` and is always sent, so all six agree on the wire.
- **Booleans on multipart must be the strings `true` and `false`.** `cpr::Part` has no bool constructor; a bool becomes `1` or `0` and the API will not treat that as a boolean. Pass `std::string("true")` or `std::string("false")`.
- **`allow_phase_2` is validation `sendFile` only**, and only when `allowPhase2IsSet` is true. Scoring `sendFile` ignores it. `activity_data` on get-file is validation only.
- **`validateBatch` uses `api.zerobounce.net`**, not `bulkapi.zerobounce.net`.
Expand Down
10 changes: 5 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `validate` and `validateBatch` take `ZBValidateOptions`: `timeout`, `activityData`, `verifyPlus`, and `usePost` (form-encoded `POST /validate`).
- `ZBValidateResponse` reads `active_in_days` and `active_first_seen`.
- `scoreEmail` (`GET /v2/scoring`) and `scoreEmailPost` (form-encoded `POST /v2/scoring`).
- Bulk email finder (`sendEmailFinderFile`, `emailFinderFileStatus`, `emailFinderGetFile`, `emailFinderDeleteFile`).
- Bulk email finder (`sendEmailFinderFile`, `emailFinderFileStatus`, `emailFinderGetFile`, `emailFinderDeleteFile`). The upload requires `domain_column` and one of `first_name_column` and `full_name_column`; neither being set is rejected before a request is sent.
- Bulk domain search (`sendDomainSearchFile`, `domainSearchFileStatus`, `domainSearchGetFile`, `domainSearchDeleteFile`).
- List evaluator (`sendListEvaluatorFile`, `getListEvaluatorResult`, `sendEvaluatedFile`).
- Filters (`addFilter`, `listFilters`, `deleteFilter`).
- S3 bulk (`validateS3File`, `scoreS3File`, `exportS3File`, `listS3files`, `emailFinderS3File`, `domainSearchS3File`).
- S3 bulk (`validateS3File`, `scoreS3File`, `exportS3File`, `listS3files`, `emailFinderS3File`, `domainSearchS3File`). All six S3 and SFTP/FTP jobs build their form from the same fields, so `validateS3File` and `scoreS3File` require `fileName` and `returnUrl` like the rest; `ValidateS3FileOptions` and `ScoreS3FileOptions` carry `returnUrl` and a plain `bool hasHeaderRow`. `exportS3File` takes an `std::optional<std::string>` bucket: leave it unset to use the export bucket from your ZeroBounce settings.
- SFTP/FTP bulk: `validateSFTPFile`, `scoreSFTPFile`, `emailFinderSFTPFile`, `domainSearchSFTPFile`, and `exportSFTPFile`, all taking `BulkFileJobOptions`. The server, path and protocol come from your ZeroBounce settings. `validateSFTPFile` is the one job whose `fileName` may be left unset; `exportSFTPFile` takes `export_path` rather than the `export_bucket` `exportS3File` sends.
- `ZBValidateOptions` accepts `domainInfo`, `greylistProcessing`, and `callbackUrl`. `callbackUrl` needs `greylistProcessing = true` and is rejected without it, and when blank, before a request is sent.
- `ZBValidateResponse` carries `domainWebsiteExists`, `domainRegistrantCompanyName`, and `message`. `message` is read from the API's `Message` key, returned while greylist processing is still running.
- Inbox Placement Tester on `https://bulkapi.zerobounce.net/api/ipt`: `getInboxPlacementProviders`, `getInboxPlacementTestTypes`, `createInboxPlacementTest`, `getInboxPlacementTest`, `listInboxPlacementTests`, `deleteInboxPlacementTest`, `getInboxPlacementWebhook`, `setInboxPlacementWebhook`.
- `setListEvaluatorBaseUrl` and `setInboxPlacementBaseUrl` override those base URLs.
- `setListEvaluatorBaseUrl`, `setInboxPlacementBaseUrl`, `setBulkApiRootUrl`, `setBulkApiBaseUrl`, and `setBulkApiScoringBaseUrl` override those base URLs. The three bulk hosts were fixed to production and had no setter, so no bulk endpoint could be pointed at a test host.
- `BaseRequestHandler` gains `Put` and `Delete`, used by the Inbox Placement Tester and the Warmup API, plus a `Delete` overload that carries a body, which the Warmup `DELETE /custom-engagement` needs. All have default implementations, so existing subclasses keep compiling.
- `ZeroBounceWarmup`, a client for the Warmup API. It is a separate product with its own host (`https://warmup-api.zerobounce.net`), its own team-scoped key sent in an `API-Key` header, and all 26 documented endpoints, including the seed-list and statistics CSV downloads.
- `ZeroBounceWarmup`, a client for the Warmup API. It is a separate product with its own host (`https://warmup-api.zerobounce.net`), its own team-scoped key sent in an `API-Key` header, and all 26 documented endpoints, including the seed-list and statistics CSV downloads. `isReady()` and `getInitializeError()` report whether `initialize` accepted the key and base URL, and why it did not, instead of leaving the client quietly unusable until the first call.
- `ZBErrorResponse::statusCode` carries the HTTP status of a failed response, so callers can tell a Cloudflare 403 from an API 400/404 and a Warmup 429.
- An opt-in live API suite, built only with `-DZEROBOUNCE_BUILD_LIVE_TESTS=ON`. A normal build and CI never compile it, and `ctest` never runs it; it needs keys in the environment, and only spends credits or changes the account behind `ZEROBOUNCE_LIVE_SPEND=1` and `ZEROBOUNCE_LIVE_CHANGES=1`.

Expand All @@ -35,7 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `validateBatch` sends `timeout` as a JSON string. Sent as a number, the live API rejects the whole batch with an account-level error.
- The Inbox Placement Tester authenticates with an `Authorization: Bearer` header instead of an `api_key` parameter, so the key no longer reaches a query string that gets logged, and `listInboxPlacementTests` calls `GET /tests`. `GET /test` answers `Method "GET" not allowed`.
- `sendEvaluatedFile(fileId)` posts the list evaluator's `file_id` as a form field. The endpoint rejects a file upload with "file_id: This field is required", so it no longer takes a file path.
- The S3 and SFTP/FTP jobs require `file_name` and `return_url`, `validateSFTPFile` included, although the docs list `file_name` as optional for that one. The API answers "This field is required" otherwise.
- All six S3 and SFTP/FTP jobs require `file_name` and `return_url`, `validateS3File`, `scoreS3File` and `validateSFTPFile` included, although the docs list them as optional. Without a return URL the API answers `Provided return URL is not permitted: None`, and without a file name, "This field is required". `validateS3File` and `scoreS3File` no longer send the undocumented singular `remove_duplicate` alongside `remove_duplicates`; `remove_duplicate` belongs to the bulk `/v2/sendfile` upload, where `SendFileOptions` still carries it.
- Query values, including the API key, email, and file id, are percent-encoded.

### Fixed
Expand Down
45 changes: 42 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -447,8 +447,30 @@ and [SFTP](https://www.zerobounce.net/docs/api-integrations/sftp_bulk_api) docs.
auto ok = [](ZBSendFileResponse response) { std::cout << response.toString() << std::endl; };
auto failed = [](ZBErrorResponse error) { std::cout << error.toString() << std::endl; };

// fileName and returnUrl are required on every one of these six jobs. The docs
// mark them optional; the live API rejects a job without a return URL with
// "Provided return URL is not permitted: None".

// Amazon S3. fileName is the full path, as returned by listS3files.
// fileName and returnUrl are required on every one of these jobs.
zb->listS3files(std::string("my-bucket"), std::string("leads"),
[](std::vector<ZBS3ListFileEntry> files) {
for (const auto& file : files) { std::cout << file.name << std::endl; }
},
failed
);

ValidateS3FileOptions validate;
validate.fileName = "lists/leads.csv";
validate.returnUrl = "https://example.com/zerobounce-done";
validate.hasHeaderRow = true;
validate.emailAddressColumn = 1; // 1-based; set it to send it, 0 included
zb->validateS3File(validate, ok, failed);

ScoreS3FileOptions score;
score.fileName = "lists/leads.csv";
score.returnUrl = "https://example.com/zerobounce-done";
zb->scoreS3File(score, ok, failed);

BulkFileJobOptions finder;
finder.fileName = "lists/leads.csv";
finder.hasHeaderRow = true;
Expand All @@ -459,6 +481,11 @@ BulkFileJobOptions domains = finder;
domains.removeDuplicates = true;
zb->domainSearchS3File(domains, ok, failed);

// export_bucket is optional: std::nullopt uses the export bucket from your
// ZeroBounce settings.
zb->exportS3File("<FILE_ID>", std::nullopt, ok, failed);
zb->exportS3File("<FILE_ID>", std::string("my-export-bucket"), ok, failed);

// SFTP/FTP. The server, path and protocol come from your ZeroBounce settings.
// The docs list fileName as optional for validateSFTPFile, but the live API
// requires it there too.
Expand All @@ -485,6 +512,12 @@ The Warmup API is a separate product. It has its own host, its own team-scoped k
ZeroBounceWarmup warmup;
warmup.initialize("<YOUR_WARMUP_API_KEY>");

// initialize rejects a padded key or a base URL that is not https, and says so
// rather than leaving the client quietly unusable.
if (!warmup.isReady()) {
std::cout << warmup.getInitializeError() << std::endl;
}

warmup.listSendingAccounts(onSuccess, onError);
warmup.getProgressToday(onSuccess, onError);
warmup.downloadSeeds(
Expand Down Expand Up @@ -541,10 +574,16 @@ Run it by hand before every release: the offline suite proves the SDK sends what
## Per-API base URLs

The list evaluator and the Inbox Placement Tester have their own base URLs,
separate from the main and bulk hosts.
separate from the main and bulk hosts. The three bulk hosts have setters of
their own, so every bulk endpoint can be pointed at a test host.

```cpp
zb->setListEvaluatorBaseUrl("https://bulkapi.zerobounce.net/v2/listevaluator");
zb->setInboxPlacementBaseUrl("https://bulkapi.zerobounce.net/api/ipt");
```

// The bulk root carries the S3 and SFTP jobs, the bulk email finder and the
// bulk domain search; the other two carry bulk validation and bulk AI scoring.
zb->setBulkApiRootUrl("https://bulkapi.zerobounce.net");
zb->setBulkApiBaseUrl("https://bulkapi.zerobounce.net/v2");
zb->setBulkApiScoringBaseUrl("https://bulkapi.zerobounce.net/v2/scoring");
```
51 changes: 42 additions & 9 deletions documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,25 +428,52 @@ and [SFTP](https://www.zerobounce.net/docs/api-integrations/sftp_bulk_api) docs.
auto ok = [](ZBSendFileResponse response) { std::cout << response.toString() << std::endl; };
auto failed = [](ZBErrorResponse error) { std::cout << error.toString() << std::endl; };

// fileName and returnUrl are required on every one of these six jobs. The docs
// mark them optional; the live API rejects a job without a return URL with
// "Provided return URL is not permitted: None".

// Amazon S3. fileName is the full path, as returned by listS3files.
zb->listS3files(std::string("my-bucket"), std::string("leads"),
[](std::vector<ZBS3ListFileEntry> files) {
for (const auto& file : files) { std::cout << file.name << std::endl; }
},
failed
);

ValidateS3FileOptions validate;
validate.fileName = "lists/leads.csv";
validate.returnUrl = "https://example.com/zerobounce-done";
validate.hasHeaderRow = true;
validate.emailAddressColumn = 1; // 1-based; set it to send it, 0 included
zb->validateS3File(validate, ok, failed);

ScoreS3FileOptions score;
score.fileName = "lists/leads.csv";
score.returnUrl = "https://example.com/zerobounce-done";
zb->scoreS3File(score, ok, failed);

BulkFileJobOptions finder;
finder.fileName = "lists/leads.csv";
finder.hasHeaderRow = true;
finder.returnUrl = "https://example.com/zerobounce-done";
zb->emailFinderS3File(finder, ok, failed);

BulkFileJobOptions domains;
domains.fileName = "lists/leads.csv";
BulkFileJobOptions domains = finder;
domains.removeDuplicates = true;
domains.returnUrl = "https://example.com/zerobounce-done";
zb->domainSearchS3File(domains, ok, failed);

// export_bucket is optional: std::nullopt uses the export bucket from your
// ZeroBounce settings.
zb->exportS3File("<FILE_ID>", std::nullopt, ok, failed);
zb->exportS3File("<FILE_ID>", std::string("my-export-bucket"), ok, failed);

// SFTP/FTP. The server, path and protocol come from your ZeroBounce settings.
// fileName may be left unset for validateSFTPFile alone, which then uses the
// input bucket from those settings.
// The docs list fileName as optional for validateSFTPFile, but the live API
// requires it there too.
BulkFileJobOptions sftp;
zb->validateSFTPFile(sftp, ok, failed);

sftp.fileName = "my_file.txt";
sftp.returnUrl = "https://example.com/zerobounce-done";
zb->validateSFTPFile(sftp, ok, failed);
zb->scoreSFTPFile(sftp, ok, failed);
zb->emailFinderSFTPFile(sftp, ok, failed);
zb->domainSearchSFTPFile(sftp, ok, failed);
Expand All @@ -459,10 +486,16 @@ zb->exportSFTPFile("<FILE_ID>", std::string("/exports"), ok, failed);
## Per-API base URLs

The list evaluator and the Inbox Placement Tester have their own base URLs,
separate from the main and bulk hosts.
separate from the main and bulk hosts. The three bulk hosts have setters of
their own, so every bulk endpoint can be pointed at a test host.

```cpp
zb->setListEvaluatorBaseUrl("https://bulkapi.zerobounce.net/v2/listevaluator");
zb->setInboxPlacementBaseUrl("https://bulkapi.zerobounce.net/api/ipt");
```

// The bulk root carries the S3 and SFTP jobs, the bulk email finder and the
// bulk domain search; the other two carry bulk validation and bulk AI scoring.
zb->setBulkApiRootUrl("https://bulkapi.zerobounce.net");
zb->setBulkApiBaseUrl("https://bulkapi.zerobounce.net/v2");
zb->setBulkApiScoringBaseUrl("https://bulkapi.zerobounce.net/v2/scoring");
```
Loading
Loading