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
18 changes: 11 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `validate_email_with_options` and `validate_email_batch_with_options` take `ZBValidateOptions`: `timeout`, `activity_data`, `verify_plus`, and `use_post` (form-encoded `POST /validate`).
- `ZBValidateResponse` reads `active_in_days` and `active_first_seen`.
- `score_email` (`GET /v2/scoring`) and `score_email_post` (form-encoded `POST /v2/scoring`).
- Bulk email finder (`send_email_finder_file`, `email_finder_file_status`, `email_finder_get_file`, `email_finder_delete_file`).
- Bulk email finder (`send_email_finder_file`, `email_finder_file_status`, `email_finder_get_file`, `email_finder_delete_file`). `send_email_finder_file` needs one of `first_name_column` and `full_name_column`, and rejects a call with neither before sending.
- Bulk domain search (`send_domain_search_file`, `domain_search_file_status`, `domain_search_get_file`, `domain_search_delete_file`).
- List evaluator (`send_list_evaluator_file`, `get_list_evaluator_result`, `send_evaluated_file`).
- Filters (`add_filter`, `list_filters`, `delete_filter`).
- S3 bulk (`validate_s3_file`, `score_s3_file`, `export_s3_file`, `list_s3_files`, `email_finder_s3_file`, `domain_search_s3_file`).
- SFTP/FTP bulk (`validate_sftp_file`, `score_sftp_file`, `email_finder_sftp_file`, `domain_search_sftp_file`, `export_sftp_file`), sharing the new `ZBBulkFileJobOptions`. The server, path and protocol come from your ZeroBounce settings. `validate_sftp_file`'s `file_name` is optional; `export_sftp_file` takes `export_path` rather than the `export_bucket` `export_s3_file` sends.
- S3 bulk (`validate_s3_file`, `score_s3_file`, `export_s3_file`, `list_s3_files`, `email_finder_s3_file`, `domain_search_s3_file`). `validate_s3_file` and `score_s3_file` require `file_name` and `return_url` like the other jobs, so `ScoreS3FileOptions` has a `return_url` field; `export_s3_file`'s `export_bucket` is optional, as `export_path` is on the SFTP twin. `ValidateS3FileOptions.remove_duplicate`, `allow_phase_2` and `allow_phase_2_is_set` are not sent: the endpoint documents `remove_duplicates`, and `allow_phase_2` belongs to the multipart bulk `send_file`.
- SFTP/FTP bulk (`validate_sftp_file`, `score_sftp_file`, `email_finder_sftp_file`, `domain_search_sftp_file`, `export_sftp_file`), sharing the new `ZBBulkFileJobOptions`. The server, path and protocol come from your ZeroBounce settings. `export_sftp_file` takes `export_path` rather than the `export_bucket` `export_s3_file` sends, and both are optional.
- `ZBValidateOptions` accepts `domain_info`, `greylist_processing` and `callback_url`. `callback_url` needs `greylist_processing` set to 1 and is rejected without it, and when blank, before a request is sent.
- `ZBValidateResponse` reads `domain_website_exists`, `domain_registrant_company_name` and `message`. `message` is the API's `Message` key, returned instead of a result while a greylist retry is still running.
- Inbox Placement Tester on `https://bulkapi.zerobounce.net/api/ipt`: `get_inbox_placement_providers`, `get_inbox_placement_test_types`, `create_inbox_placement_test`, `get_inbox_placement_test`, `list_inbox_placement_tests`, `delete_inbox_placement_test`, `get_inbox_placement_webhook`, `set_inbox_placement_webhook`. It authenticates with an `Authorization: Bearer` header, bodies are camelCase JSON, and unset fields are left out.
- `zero_bounce_set_list_evaluator_base_url` and `zero_bounce_set_inbox_placement_base_url`.
- Inbox Placement Tester on `https://bulkapi.zerobounce.net/api/ipt`: `get_inbox_placement_providers`, `get_inbox_placement_test_types`, `create_inbox_placement_test`, `get_inbox_placement_test`, `list_inbox_placement_tests`, `delete_inbox_placement_test`, `get_inbox_placement_webhook`, `set_inbox_placement_webhook`. It authenticates with an `Authorization: Bearer` header, bodies are camelCase JSON, and unset fields are left out. Every value in a body is JSON-escaped and the bodies grow to fit, so a quote, a backslash or a long URL can no longer break or truncate one.
- `ZBCreateInboxPlacementTestOptions` carries `provider_ids`, `seed_ids` and `providers` (a `ZBInboxPlacementProvider` array of `id` plus `max_seeds`), sent as `providerIds`, `seedIds` and `providers`. `create_inbox_placement_test` requires a `name` of at most 64 characters, a `test_type` of 1, 2 or 3, and a `sender_email` when `test_type` is 1.
- `ZBListInboxPlacementTestsOptions` carries the documented `created_after` and `created_before` filters, and `page_size` has to be between 1 and 100.
- `zero_bounce_set_list_evaluator_base_url`, `zero_bounce_set_inbox_placement_base_url`, `zero_bounce_set_bulk_api_base_url`, `zero_bounce_set_bulk_api_root_url` and `zero_bounce_set_bulk_api_scoring_base_url`. The bulk hosts are not built from `api_base_url`, so until now there was no way to move them, although the regional-host warning told you to.
- `zb_make_request_with_method_and_body` and `zb_make_request_with_extra_header`, for the Tester's PUT and DELETE and its Bearer header.
- `ZeroBounceWarmup`, a client for the [Warmup API](https://www.zerobounce.net/docs/warmup-api): a separate product with its own host, its own team-scoped key (`zero_bounce_warmup_initialize`, sent in an `API-Key` header) and all 26 documented endpoints. Every call hands the raw JSON body to its callback, or an empty string for the documented empty successes (HTTP 204 and an empty 200); `warmup_download_seeds` and `warmup_download_statistics` hand over CSV text and write it to a path when one is given.
- `ZBErrorResponse.status_code` carries the HTTP status the request came back with, so a caller can tell 429 from 500 without parsing the message. It is 0 when the error was raised before a response arrived.
Expand All @@ -34,10 +36,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Verified against the live API. Where it disagrees with the docs, the API wins and the call site says so:
- `validate_email_batch_with_options` sends `timeout` as a JSON **string**. Sent as a number the API rejects the whole batch with the account-level "Invalid API Key or your account ran out of credits".
- The Inbox Placement Tester authenticates with `Authorization: Bearer`, not the `api_key` query parameter the rest of the API takes, and its listing is `GET /tests` (plural) — `/test` answers `Method "GET" not allowed`.
- Every S3/SFTP job requires both `file_name` and `return_url`, `validate_sftp_file` included, although the docs call its `file_name` optional. Both are checked before the request is sent.
- Every S3/SFTP job requires both `file_name` and `return_url`, `validate_s3_file`, `score_s3_file` and `validate_sftp_file` included, although the docs call several of them optional. `POST /validateS3file` without a `return_url` answers 400 `Provided return URL is not permitted: None`, so both are checked before the request is sent.
- `list_inbox_placement_tests` sends `page_size`. `pageSize`, which the SDK used to send, is accepted and then silently ignored, so a caller asking for a given page size quietly got the default page.
- **Breaking:** `send_evaluated_file`'s second argument is the id of a finished evaluation, not a path to upload. The live endpoint takes the file id as a form field.
- A `"success": false` field on an HTTP 200 now reaches the error callback instead of being returned as a result. `get_file` and `delete_file` report an unknown or deleted file id that way.
- The list evaluator uses its own base URL (`.../v2/listevaluator`) instead of building the path from the bulk base, and the file id is encoded as a single path segment. `send_evaluated_file` stays on the bulk base.
- The list evaluator uses its own base URL (`.../v2/listevaluator`) instead of building the path from the bulk base, for the upload as well as the result, and the file id is encoded as a single path segment. `send_list_evaluator_file` used to ignore `zero_bounce_set_list_evaluator_base_url` and build its URL from the bulk base, which split the two halves across different hosts. `send_evaluated_file` stays on the bulk base.

- API requests time out after 120 seconds.
- Query values, including the API key, email, and file id, are percent-encoded.
Expand All @@ -56,6 +59,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Bulk email finder and domain search send `has_header_row` when it is set to false.
- `validate_s3_file` sends `email_address_column` when it is set to 0. `validate_s3_file`, `score_s3_file`, and `export_s3_file` reject an empty API key before a request is sent.
- `validate_email_batch` sends an empty IP address as JSON `null`.
- `send_file` and `scoring_send_file` no longer dereference a NULL `SendFileOptions.return_url`. A caller that zeroed the struct instead of calling `new_send_file_options` crashed on the request path.

### Security

Expand Down
47 changes: 42 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,9 +391,11 @@ SFTP/FTP server. See the [S3](https://www.zerobounce.net/docs/api-integrations/a
and [SFTP](https://www.zerobounce.net/docs/api-integrations/sftp_bulk_api) docs.
These live on the bulkapi host *without* the `/v2` path.
Every one of these jobs needs both `file_name` and `return_url`. The docs call
`validate_sftp_file`'s `file_name` optional, but the live API answers
`Empty parameter: file_name` without it, so the SDK checks both before sending.
Every one of these jobs needs both `file_name` and `return_url`, `validate_s3_file`
and `score_s3_file` included. The docs call several of them optional, but the live
API answers `Empty parameter: file_name` without the one and
`Provided return URL is not permitted: None` without the other, so the SDK checks
both before sending.
```c
ZBBulkFileJobOptions options = new_bulk_file_job_options();
Expand All @@ -418,6 +420,22 @@ domain_search_sftp_file(zb, &options, on_send, on_error);
/* Note the field: this one takes export_path, not export_bucket.
NULL uses the export path from your settings. */
export_sftp_file(zb, "<FILE_ID>", "/exports", on_send, on_error);
/* validate_s3_file and score_s3_file take the same two required fields plus
their own columns. */
ValidateS3FileOptions validate = new_validate_s3_file_options();
validate.file_name = "lists/leads.csv";
validate.return_url = "https://example.com/zerobounce-done";
validate.email_address_column = 1;
validate_s3_file(zb, &validate, on_send, on_error);
ScoreS3FileOptions score = new_score_s3_file_options();
score.file_name = "lists/leads.csv";
score.return_url = "https://example.com/zerobounce-done";
score_s3_file(zb, &score, on_send, on_error);
/* export_bucket is optional: NULL uses the one from your settings. */
export_s3_file(zb, "<FILE_ID>", NULL, on_send, on_error);
```

## Inbox Placement Tester
Expand All @@ -430,10 +448,20 @@ the API takes, and it spends the account's inbox-test credits.
get_inbox_placement_providers(zb, on_raw, on_error);
get_inbox_placement_test_types(zb, on_raw, on_error);

/* name is at most 64 characters, test_type is 1, 2 or 3, and a test_type of 1
needs a sender_email. */
static const int provider_ids[] = {3, 7};
static const ZBInboxPlacementProvider providers[] = {{3, 10}};

ZBCreateInboxPlacementTestOptions create = new_create_inbox_placement_test_options();
create.test_type = 1;
create.sender_email = "news@example.com";
create.webhook_url = "https://example.com/ipt-webhook";
/* Optional: pick the providers, the seeds, or a per-provider seed cap. */
create.provider_ids = provider_ids;
create.provider_ids_count = 2;
create.providers = providers;
create.providers_count = 1;
create_inbox_placement_test(zb, "Launch", &create, on_raw, on_error);

/* Send your campaign to the seeds you got back, then read the results. */
Expand All @@ -442,6 +470,9 @@ get_inbox_placement_test(zb, "<TEST_ID>", on_raw, on_error);
ZBListInboxPlacementTestsOptions list = new_list_inbox_placement_tests_options();
list.status = "finished";
list.page = 1;
list.page_size = 25; /* 1 to 100 */
list.created_after = "2026-01-01";
list.created_before = "2026-02-01";
list_inbox_placement_tests(zb, &list, on_raw, on_error);

set_inbox_placement_webhook(zb, "https://example.com/ipt-webhook", on_raw, on_error);
Expand Down Expand Up @@ -512,10 +543,16 @@ SMTP login with HTTP 500, which arrives as an error with
## Per-API base URLs
The list evaluator and the Inbox Placement Tester are separate hosts, so
`zero_bounce_initialize_with_base_url` alone leaves them pointing at the live API.
The bulk hosts, the list evaluator and the Inbox Placement Tester are not built
from `api_base_url`, so `zero_bounce_initialize_with_base_url` alone leaves them
pointing at the live API. Each one has a setter. They keep the pointer they are
given rather than a copy, so the string has to outlive the client.
```c
zero_bounce_set_bulk_api_base_url(zb, "https://bulkapi.zerobounce.net/v2");
zero_bounce_set_bulk_api_scoring_base_url(zb, "https://bulkapi.zerobounce.net/v2/scoring");
/* The S3 and SFTP jobs use the bulk host root, without /v2. */
zero_bounce_set_bulk_api_root_url(zb, "https://bulkapi.zerobounce.net");
zero_bounce_set_list_evaluator_base_url(zb, "https://bulkapi.zerobounce.net/v2/listevaluator");
zero_bounce_set_inbox_placement_base_url(zb, "https://bulkapi.zerobounce.net/api/ipt");
```
Expand Down
47 changes: 42 additions & 5 deletions documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,9 +379,11 @@ SFTP/FTP server. See the [S3](https://www.zerobounce.net/docs/api-integrations/a
and [SFTP](https://www.zerobounce.net/docs/api-integrations/sftp_bulk_api) docs.
These live on the bulkapi host *without* the `/v2` path.
Every one of these jobs needs both `file_name` and `return_url`. The docs call
`validate_sftp_file`'s `file_name` optional, but the live API answers
`Empty parameter: file_name` without it, so the SDK checks both before sending.
Every one of these jobs needs both `file_name` and `return_url`, `validate_s3_file`
and `score_s3_file` included. The docs call several of them optional, but the live
API answers `Empty parameter: file_name` without the one and
`Provided return URL is not permitted: None` without the other, so the SDK checks
both before sending.
```c
ZBBulkFileJobOptions options = new_bulk_file_job_options();
Expand All @@ -406,6 +408,22 @@ domain_search_sftp_file(zb, &options, on_send, on_error);
/* Note the field: this one takes export_path, not export_bucket.
NULL uses the export path from your settings. */
export_sftp_file(zb, "<FILE_ID>", "/exports", on_send, on_error);
/* validate_s3_file and score_s3_file take the same two required fields plus
their own columns. */
ValidateS3FileOptions validate = new_validate_s3_file_options();
validate.file_name = "lists/leads.csv";
validate.return_url = "https://example.com/zerobounce-done";
validate.email_address_column = 1;
validate_s3_file(zb, &validate, on_send, on_error);
ScoreS3FileOptions score = new_score_s3_file_options();
score.file_name = "lists/leads.csv";
score.return_url = "https://example.com/zerobounce-done";
score_s3_file(zb, &score, on_send, on_error);
/* export_bucket is optional: NULL uses the one from your settings. */
export_s3_file(zb, "<FILE_ID>", NULL, on_send, on_error);
```

## Inbox Placement Tester
Expand All @@ -418,10 +436,20 @@ the API takes, and it spends the account's inbox-test credits.
get_inbox_placement_providers(zb, on_raw, on_error);
get_inbox_placement_test_types(zb, on_raw, on_error);

/* name is at most 64 characters, test_type is 1, 2 or 3, and a test_type of 1
needs a sender_email. */
static const int provider_ids[] = {3, 7};
static const ZBInboxPlacementProvider providers[] = {{3, 10}};

ZBCreateInboxPlacementTestOptions create = new_create_inbox_placement_test_options();
create.test_type = 1;
create.sender_email = "news@example.com";
create.webhook_url = "https://example.com/ipt-webhook";
/* Optional: pick the providers, the seeds, or a per-provider seed cap. */
create.provider_ids = provider_ids;
create.provider_ids_count = 2;
create.providers = providers;
create.providers_count = 1;
create_inbox_placement_test(zb, "Launch", &create, on_raw, on_error);

/* Send your campaign to the seeds you got back, then read the results. */
Expand All @@ -430,6 +458,9 @@ get_inbox_placement_test(zb, "<TEST_ID>", on_raw, on_error);
ZBListInboxPlacementTestsOptions list = new_list_inbox_placement_tests_options();
list.status = "finished";
list.page = 1;
list.page_size = 25; /* 1 to 100 */
list.created_after = "2026-01-01";
list.created_before = "2026-02-01";
list_inbox_placement_tests(zb, &list, on_raw, on_error);

set_inbox_placement_webhook(zb, "https://example.com/ipt-webhook", on_raw, on_error);
Expand Down Expand Up @@ -500,10 +531,16 @@ SMTP login with HTTP 500, which arrives as an error with
## Per-API base URLs
The list evaluator and the Inbox Placement Tester are separate hosts, so
`zero_bounce_initialize_with_base_url` alone leaves them pointing at the live API.
The bulk hosts, the list evaluator and the Inbox Placement Tester are not built
from `api_base_url`, so `zero_bounce_initialize_with_base_url` alone leaves them
pointing at the live API. Each one has a setter. They keep the pointer they are
given rather than a copy, so the string has to outlive the client.
```c
zero_bounce_set_bulk_api_base_url(zb, "https://bulkapi.zerobounce.net/v2");
zero_bounce_set_bulk_api_scoring_base_url(zb, "https://bulkapi.zerobounce.net/v2/scoring");
/* The S3 and SFTP jobs use the bulk host root, without /v2. */
zero_bounce_set_bulk_api_root_url(zb, "https://bulkapi.zerobounce.net");
zero_bounce_set_list_evaluator_base_url(zb, "https://bulkapi.zerobounce.net/v2/listevaluator");
zero_bounce_set_inbox_placement_base_url(zb, "https://bulkapi.zerobounce.net/api/ipt");
```
Expand Down
26 changes: 25 additions & 1 deletion include/ZeroBounce/ZBPriority3Options.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
#include <stdbool.h>

/**
* Optional columns for bulk email finder sendfile.
* Columns for bulk email finder sendfile.
* Column indexes start at 1. has_header_row: -1 omit, 0 sends "false", 1 sends "true".
*
* One of first_name_column and full_name_column is required: the endpoint has
* no name to search for without it.
*/
typedef struct {
int first_name_column;
Expand All @@ -19,27 +22,48 @@ typedef struct {
int has_header_row;
} DomainSearchSendFileOptions;

/**
* Options for validate_s3_file. file_name and return_url are both required:
* without a return_url the API answers
* "Provided return URL is not permitted: None".
*
* remove_duplicate, allow_phase_2 and allow_phase_2_is_set are no longer sent.
* validateS3file documents remove_duplicate**s**, and allow_phase_2 belongs to
* the multipart bulk sendfile, not to this job. They are kept so existing code
* still compiles.
*/
typedef struct {
const char *file_name;
int has_header_row;
int remove_duplicates;
/** Deprecated: not sent. Use remove_duplicates. */
int remove_duplicate;
int email_address_column;
/** Required. */
const char *return_url;
int first_name_column;
int last_name_column;
int gender_column;
int ip_address_column;
/** Deprecated: not sent. */
bool allow_phase_2_is_set;
/** Deprecated: not sent. */
bool allow_phase_2;
} ValidateS3FileOptions;

/**
* Options for score_s3_file. file_name and return_url are both required, as
* they are for every other S3 and SFTP file job.
*/
typedef struct {
const char *file_name;
int has_header_row;
int remove_duplicates;
/** Deprecated: not sent. Use remove_duplicates. */
int remove_duplicate;
int email_address_column;
/** Required. */
const char *return_url;
} ScoreS3FileOptions;

EmailFinderSendFileOptions new_email_finder_send_file_options(void);
Expand Down
Loading
Loading