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
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ CI (`.github/workflows/sdk_ci.yml`) runs `ruff check`, `compileall`, then `pytho
- **Column indexes are 1-based, and the SDK does not add 1.** Pass the API's index. `0` is a real value and is sent. `None` on an optional column (such as `first_name_column`) omits the field. `send_file`'s email column is required and is sent as given, including `0`. `domain_column` and the list-evaluator email column are required: `None` is rejected, `0` is sent. The bulk email finder also needs `first_name_column` or `full_name_column`.
- **Optional parameters are omitted when unset.** `timeout`, `activity_data`, `verify_plus`, `domain_info`, `greylist_processing`, `callback_url`, and `allow_phase_2` are not sent until the caller sets them. Explicit `False` is sent as `false`. `ip_address=None` is omitted; `""` is sent.
- **Booleans on form and multipart bodies must be the strings `true` and `false`.** A Python `bool` passed to `requests` becomes `True` or `False`, and the API will not treat that as a boolean. `_form_data` converts them. JSON bodies (`validate_batch`, the Inbox Placement Tester, the Warmup API) keep JSON booleans.
- **`allow_phase_2` is validation `send_file` only.** Scoring `send_file` and the S3 / SFTP jobs do not send it; those jobs send only their documented `file_name`, `has_header_row`, `remove_duplicates`, and `return_url`. `activity_data` on get-file is validation `get_file` only.
- **`allow_phase_2` is validation `send_file` only.** Scoring `send_file` and the S3 / SFTP jobs do not send it; those jobs send only `file_name`, `has_header_row`, `remove_duplicates`, and `return_url`, and require `file_name`, `has_header_row` and `return_url` because the live API rejects jobs without them. `activity_data` on get-file is validation `get_file` only.
- **`validate_batch` uses `api.zerobounce.net`**, not `bulkapi.zerobounce.net`. It follows `base_url`.
- **Failures raise.** `ZBClientException` means the arguments were rejected and no request was sent. `ZBApiException` means the API failed. The message must be short: no raw HTML, no megabyte body, no `JSONDecodeError` / `Expecting value` text, and no `Error: Error:` prefix. An account-level `validate_batch` error (`email_address` `all`) raises. A per-address error stays on the result's `errors` list. `get_credits` with an invalid key returns `Credits` `-1`; that endpoint does not send an `error` field.
- **Downloads are buffered unless `stream=True`.** The streamed path sniffs the first 64 KB for an error body, writes to a `.part` temporary file next to `download_path`, and moves it into place only when complete. Keep both paths' error checks identical (`_raise_if_download_error`).
Expand All @@ -50,6 +50,8 @@ CI (`.github/workflows/sdk_ci.yml`) runs `ruff check`, `compileall`, then `pytho
- **Do not commit `.env`.** It is gitignored and may hold a real key. Tests must not call the live API.
- Match the surrounding style. Do not reformat unrelated code.

- **The live API wins over the docs.** Where they disagree, follow what the API accepts and say so in the docstring. Known cases: `validate_batch` `timeout` must be a JSON string; `send_evaluated_file` takes the evaluator's `file_id`, not an upload; S3 / SFTP jobs need `return_url` (and `validateSFTPfile` needs `file_name`); Warmup `check-domain-dmarc` needs an email address, not a domain. Check a new endpoint against the live API (`tests/zero_bounce_integration_test.py` style, sandbox addresses where possible) before release.

## 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).
Expand Down
11 changes: 6 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- `validate` takes `timeout` (3–60 seconds), `activity_data`, `verify_plus`, and `use_post` (form-encoded `POST /validate`).
- `validate_batch` takes `timeout` (10–120 seconds), `activity_data`, and `verify_plus`.
- `validate_batch` takes `timeout` (10–120 seconds; sent as a string, which the live API requires), `activity_data`, and `verify_plus`.
- `ZBValidateResponse` reads `active_in_days` and `active_first_seen`.
- `score_email` (`GET /scoring`) and `score_email(..., use_post=True)` (form-encoded `POST /scoring`).
- Bulk email finder (`send_email_finder_file`, `email_finder_file_status`, `email_finder_get_file`, `email_finder_delete_file`, plus stream upload).
- Bulk domain search (`send_domain_search_file`, `domain_search_file_status`, `domain_search_get_file`, `domain_search_delete_file`, plus stream upload).
- List evaluator (`send_list_evaluator_file`, `get_list_evaluator_result`, `send_evaluated_file`). Uploads and results return `ZBListEvaluatorResponse` (`file_id`, `status`, `progress`, the percentages, `abuse`, `do_not_mail`, `spam_trap`).
- List evaluator (`send_list_evaluator_file`, `get_list_evaluator_result`, `send_evaluated_file`). `send_evaluated_file(file_id)` submits a finished evaluation for validation by the evaluator's `file_id`, as the live API requires (the docs describe a file upload). Uploads and results return `ZBListEvaluatorResponse` (`file_id`, `status`, `progress`, the percentages, `abuse`, `do_not_mail`, `spam_trap`).
- Filters (`add_filter`, `list_filters`, `delete_filter`). `rule` must be `allow` or `block` and `target` `email`, `domain`, `mx`, or `tld`.
- Amazon S3 bulk (`list_s3files`, `validate_s3file`, `score_s3file`, `email_finder_s3file`, `domain_search_s3file`, `export_s3file`) and SFTP/FTP bulk (`validate_sftpfile`, `score_sftpfile`, `email_finder_sftpfile`, `domain_search_sftpfile`, `export_sftpfile`). Each job sends the documented `file_name`, `has_header_row` (required), `remove_duplicates`, and `return_url`. The export bucket and path are optional; without them your account settings are used.
- Amazon S3 bulk (`list_s3files`, `validate_s3file`, `score_s3file`, `email_finder_s3file`, `domain_search_s3file`, `export_s3file`) and SFTP/FTP bulk (`validate_sftpfile`, `score_sftpfile`, `email_finder_sftpfile`, `domain_search_sftpfile`, `export_sftpfile`). Each job sends `file_name`, `has_header_row`, `remove_duplicates`, and `return_url`; `file_name`, `has_header_row`, and `return_url` are required because the live API rejects jobs without them. The export bucket and path are optional; without them your account settings are used.
- `validate` takes `domain_info` (website availability and WHOIS), `greylist_processing`, and `callback_url` (requires `greylist_processing=True`). `ZBValidateResponse` reads `domain_website_exists` and `domain_registrant_company_name`, and `message` while a greylist retry is still in progress.
- Inbox Placement Tester (`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`) on `bulkapi.zerobounce.net/api/ipt`. Its `detail` error messages raise `ZBApiException` with that text.
- `get_credits`, `get_api_usage`, `get_activity`, `find_email_format`, and `find_domain` take `use_post=True` (form-encoded `POST`, which the API accepts since September 2026).
- `ZBValidateSubStatus.ai_agent_mailbox`, documented for `valid` and `catch-all` results.
- `ZBGetActivityResponse.active_first_seen`, and `ZBGetApiUsageResponse` counts for `leading_period_removed` and `alias_address`.
- `ZeroBounceWarmup`, a client for the Warmup API (https://www.zerobounce.net/docs/warmup-api) with a method for each of its 26 endpoints. It sends a Warmup API key in the `API-Key` header to `https://warmup-api.zerobounce.net` (or the base URL you pass), checks the documented ranges before sending, returns `None` for 204 and empty successes, and returns CSV text for the two downloads.
- `ZeroBounceWarmup`, a client for the Warmup API (https://www.zerobounce.net/docs/warmup-api) with a method for each of its 26 endpoints. `check_domain_dmarc` takes an email address, as the live API requires. It sends a Warmup API key in the `API-Key` header to `https://warmup-api.zerobounce.net` (or the base URL you pass), checks the documented ranges before sending, returns `None` for 204 and empty successes, and returns CSV text for the two downloads.
- `get_file`, `scoring_get_file`, `email_finder_get_file`, and `domain_search_get_file` take `stream=True` to write a result file in chunks instead of reading it into memory first. The file appears at `download_path` only once complete, so a failed download leaves an existing file untouched and no partial file behind. Without the flag, downloads behave as before.
- A `UserWarning`, once per client, when an EU or US `base_url` is set but a request goes to `bulkapi.zerobounce.net` (bulk, scoring and finder files, the list evaluator, S3/SFTP, the Inbox Placement Tester): ZeroBounce documents no regional bulk host.
- `ZBApiException.status_code`: the HTTP status, set by `ZeroBounceWarmup` so callers can back off on 429.
- `ZBApiException.status_code`: the HTTP status of a failed response, from both clients (for example 429 from the Warmup API, 403 when Cloudflare blocks a request).
- `LIST_EVALUATOR_BASE_URL` and `INBOX_PLACEMENT_BASE_URL` set where the list evaluator and Inbox Placement Tester requests go, like `BULK_BASE_URL` for the other bulk files.

### Changed
Expand All @@ -50,6 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Importing the SDK works on Python 3.7 to 3.9 again. `ZeroBounce.__init__` was annotated with `timedelta | None`, which raised `TypeError` at import on those versions.
- `find_domain` and `guess_format` raise `ZBApiException` for a lowercase `message` error instead of `KeyError`.
- A validation error body `{"detail": [{"loc": ..., "msg": ...}]}` raises with the field and message instead of the raw JSON.
- An error body that is a JSON list of messages (`["In-progress tests cannot be deleted."]`) raises with the first message instead of the raw JSON.
- An unknown `email_confidence` or `confidence` value is `None` with a warning instead of a `ValueError`.
- `ZBValidateResponse` reads `free_email`, `mx_found`, and `catchall_domain` sent as the strings `"true"` / `"false"` as booleans (the documented samples send `mx_found` as a string).
- `processed_at` without fractional seconds, and `get_api_usage` dates in the documented `yyyy/mm/dd` form, parse instead of raising `ValueError`. A date in another format is kept as the original string, with a warning.
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ except ZBException as e:
```python
sent = zero_bounce.send_list_evaluator_file("./list.csv", email_address_column=1) # at least 100 emails
result = zero_bounce.get_list_evaluator_result(sent.file_id) # .status (processing/finished), .progress, .invalid_percentage, ...
zero_bounce.send_evaluated_file("./list.csv") # submit the evaluated list for validation
job = zero_bounce.send_evaluated_file(sent.file_id) # validate the evaluated list; job.file_id -> file_status / get_file
```

### Allowlist and blocklist filters
Expand All @@ -492,7 +492,7 @@ try:

zero_bounce.validate_s3file(name, True, return_url="https://example.com/done")
# Also: score_s3file, email_finder_s3file, domain_search_s3file (file_name, has_header_row, ...)
zero_bounce.score_sftpfile("my_file.txt", True) # file name on your SFTP/FTP server
zero_bounce.score_sftpfile("my_file.txt", True, return_url="https://example.com/done") # file on your SFTP/FTP server
# Also: validate_sftpfile, email_finder_sftpfile, domain_search_sftpfile

# When the file is processed, export it back with its file ID (from return_url's callback,
Expand All @@ -503,7 +503,7 @@ except ZBException as e:
print("ZeroBounce S3/SFTP error: " + str(e))
```

`has_header_row` is required; `remove_duplicates` (default `True` on the API) and `return_url` are optional. The documented success response is `success` and `message`, without a file ID.
`file_name`, `has_header_row` and `return_url` are required: the live API rejects a job without a return URL, and `validate_sftpfile` without a file name, although the docs mark them optional. `remove_duplicates` (default `True` on the API) is optional. The documented success response is `success` and `message`, without a file ID.

### Inbox Placement Tester

Expand Down Expand Up @@ -553,7 +553,7 @@ try:
managed_sending={"weekdays_only": True, "timezone_offset": 3.0, "warmup_start_date": "2026-11-02"},
)
print(warmup.get_sending_account("sender@example.com"))
print(warmup.check_domain_dmarc("example.com", dkim_selector="default"))
print(warmup.check_domain_dmarc("sender@example.com", dkim_selector="default")) # an email address, not a domain
warmup.set_engagement_rule("sender@example.com", 1) # returns None (HTTP 204)
warmup.create_custom_engagement("sender@example.com", reply_pct=20, important_pct=25, click_pct=25)
print(warmup.get_statistics_per_day("sender@example.com", "2026-10-01", "2026-10-14"))
Expand All @@ -572,6 +572,8 @@ Every documented endpoint has a method: sending accounts (`list_sending_accounts

### Other hosts

`test_smtp_configuration` currently gets HTTP 500 from the API whenever the SMTP login fails, so a `ZBApiException` with `status_code` 500 there means "the credentials did not work".

`base_url` sets the host of the single-request API (validate, scoring, email finder, filters). The bulk APIs have their own class attributes, which you can set on an instance. ZeroBounce documents no regional bulk host, so with an EU or US `base_url` the bulk calls still go to `bulkapi.zerobounce.net`; the client warns once (`UserWarning`) when that happens, unless you set the bulk URLs yourself.

```python
Expand Down
10 changes: 6 additions & 4 deletions documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ except ZBException as e:
```python
sent = zero_bounce.send_list_evaluator_file("./list.csv", email_address_column=1) # at least 100 emails
result = zero_bounce.get_list_evaluator_result(sent.file_id) # .status (processing/finished), .progress, .invalid_percentage, ...
zero_bounce.send_evaluated_file("./list.csv") # submit the evaluated list for validation
job = zero_bounce.send_evaluated_file(sent.file_id) # validate the evaluated list; job.file_id -> file_status / get_file
```

##### Allowlist and blocklist filters
Expand All @@ -474,7 +474,7 @@ try:

zero_bounce.validate_s3file(name, True, return_url="https://example.com/done")
# Also: score_s3file, email_finder_s3file, domain_search_s3file (file_name, has_header_row, ...)
zero_bounce.score_sftpfile("my_file.txt", True) # file name on your SFTP/FTP server
zero_bounce.score_sftpfile("my_file.txt", True, return_url="https://example.com/done") # file on your SFTP/FTP server
# Also: validate_sftpfile, email_finder_sftpfile, domain_search_sftpfile

# When the file is processed, export it back with its file ID (from return_url's callback,
Expand All @@ -485,7 +485,7 @@ except ZBException as e:
print("ZeroBounce S3/SFTP error: " + str(e))
```

`has_header_row` is required; `remove_duplicates` (default `True` on the API) and `return_url` are optional. The documented success response is `success` and `message`, without a file ID.
`file_name`, `has_header_row` and `return_url` are required: the live API rejects a job without a return URL, and `validate_sftpfile` without a file name, although the docs mark them optional. `remove_duplicates` (default `True` on the API) is optional. The documented success response is `success` and `message`, without a file ID.

##### Inbox Placement Tester

Expand Down Expand Up @@ -535,7 +535,7 @@ try:
managed_sending={"weekdays_only": True, "timezone_offset": 3.0, "warmup_start_date": "2026-11-02"},
)
print(warmup.get_sending_account("sender@example.com"))
print(warmup.check_domain_dmarc("example.com", dkim_selector="default"))
print(warmup.check_domain_dmarc("sender@example.com", dkim_selector="default")) # an email address, not a domain
warmup.set_engagement_rule("sender@example.com", 1) # returns None (HTTP 204)
warmup.create_custom_engagement("sender@example.com", reply_pct=20, important_pct=25, click_pct=25)
print(warmup.get_statistics_per_day("sender@example.com", "2026-10-01", "2026-10-14"))
Expand All @@ -554,6 +554,8 @@ Every documented endpoint has a method: sending accounts (`list_sending_accounts

##### Other hosts

`test_smtp_configuration` currently gets HTTP 500 from the API whenever the SMTP login fails, so a `ZBApiException` with `status_code` 500 there means "the credentials did not work".

`base_url` sets the host of the single-request API (validate, scoring, email finder, filters). The bulk APIs have their own class attributes, which you can set on an instance. ZeroBounce documents no regional bulk host, so with an EU or US `base_url` the bulk calls still go to `bulkapi.zerobounce.net`; the client warns once (`UserWarning`) when that happens, unless you set the bulk URLs yourself.

```python
Expand Down
15 changes: 11 additions & 4 deletions src/zerobouncesdk/zb_warmup.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,12 +371,17 @@ def get_from_address_statistics(self, from_address: str):
"GET", "/from-address-statistics", params={"from_address": self._required(from_address, "from_address")}
)

def check_domain_dmarc(self, domain: str, dkim_selector: Optional[str] = None):
"""Check a domain's DNS records (GET /check-domain-dmarc).
def check_domain_dmarc(self, from_address: str, dkim_selector: Optional[str] = None):
"""SPF, DKIM and DMARC status of a from-address's domain (GET /check-domain-dmarc).

The API names the domain parameter ``from_address``; pass a domain such as ``example.com``.
Pass an email address: the live API answers "Please provide a valid email address" for a bare
domain, although the docs describe a domain. Returns ``SPF_status``, ``DKIM_status``,
``DMARC_status`` and the ``dkim_selector`` that was checked.
"""
params = {"from_address": self._required(domain, "domain")}
self._required(from_address, "from_address")
if "@" not in from_address:
raise ZBClientException("Invalid parameter: from_address (an email address, e.g. sender@example.com)")
params = {"from_address": from_address}
if dkim_selector is not None:
params["dkim_selector"] = self._required(dkim_selector, "dkim_selector")
return self._json("GET", "/check-domain-dmarc", params=params)
Expand All @@ -385,6 +390,8 @@ def test_smtp_configuration(self, from_address: str, username: str, password: st
"""Check that SMTP credentials connect and hand off a message (POST /test-smtp-configuration).

Nothing is stored, and the test message goes to a ZeroBounce test recipient, not ``from_address``.
The live API currently answers a failed SMTP login with HTTP 500, raised as ``ZBApiException``
with ``status_code`` 500.
"""
if isinstance(port, bool) or not isinstance(port, int) or not 1 <= port <= 65535:
raise ZBClientException("Invalid parameter: port (must be 1 to 65535)")
Expand Down
Loading
Loading