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
13 changes: 8 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Guidance for AI coding agents working in this repository.
- `src/zerobouncesdk/zb_warmup.py`: `ZeroBounceWarmup`, the Warmup API client. It has its own host and key, authenticates with an `API-Key` header, and reuses the error helpers in `zerobouncesdk.py`. Keep Warmup methods there, not on `ZeroBounce`.
- `src/zerobouncesdk/zb_*.py`: response models, enums, and options.
- `tests/zero_bounce_test_case.py`: scripted unit tests. They patch `zerobouncesdk.zerobouncesdk.requests`.
- `tests/test_offline_edges.py` and `tests/test_warmup_offline.py`: more offline suites on the same principle (error bodies, redaction, every optional field; the Warmup API).
- `tests/test_offline_edges.py`, `tests/test_warmup_offline.py`, `tests/test_security_offline.py` and `tests/test_streaming_and_regions_offline.py`: more offline suites on the same principle (error bodies and every optional field; the Warmup API; where keys travel and that they never reach an exception; streamed downloads and the regional-host warning).
- `tests/test_offline_api.py`: offline suite. It patches `requests.sessions.Session.send`, so the real `requests` client builds the URL and body and the fake answers without the network.
- `tests/zero_bounce_integration_test.py`: live API tests. `pytest` does not collect this file. Run it with `python -m unittest tests.zero_bounce_integration_test` when `ZEROBOUNCE_API_KEY` is set.
- `pyproject.toml`: the only version field, `version = "x.y.z"`. `setup.py` calls `setup()` and reads that file.
Expand All @@ -21,10 +21,10 @@ Guidance for AI coding agents working in this repository.

```bash
pip install -e ".[test]"
pip install ruff
pip install ruff==0.17.2 # the version CI pins
ruff check src tests # undefined names and syntax errors only
python -m compileall -q src tests
python -m pytest # unit tests and tests/test_offline_api.py
python -m pytest # every offline suite under tests/
```

`ruff check` plus `compileall` is the lint step. There is no typechecker. The Ruff selection in `pyproject.toml` is `E9`, `F63`, `F7`, and `F82`. Do not widen it to a style ruleset that fails the existing tree.
Expand All @@ -36,13 +36,16 @@ CI (`.github/workflows/sdk_ci.yml`) runs `ruff check`, `compileall`, then `pytho
- **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. Integration pages count: the S3 bulk API is documented at https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api.
- **One `ZeroBounce` per API key.** The class is not a singleton. Do not reuse one instance across users or requests. The offline concurrency test constructs a separate client per key.
- **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`, 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`) keep JSON booleans.
- **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.
- **`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`).
- **A file download is not JSON.** `get_file` saves a non-empty CSV body, including `text/csv` and `text/plain`. An empty body, an HTML page (status 200 or 403), and a JSON error are `ZBApiException`. Do not treat every `text/plain` body as an error.
- **Where the key travels is fixed by each API's docs.** The validation API: `api_key` in the query (GET) or body (POST, uploads). The Inbox Placement Tester: `Authorization: Bearer` (`_request_json(..., bearer=True)`). The Warmup API: `API-Key` header, in `ZeroBounceWarmup` only, never mixed with `ZeroBounce`.
- **Every request URL must be https.** `_checked_url` (main client) refuses anything else before sending, including the reassignable `*_BASE_URL` attributes; it also warns once when an EU or US `base_url` meets `bulkapi.zerobounce.net`, because no regional bulk host is documented.
- **Keys never reach an exception.** `_check_api_key` rejects surrounding whitespace and control characters (the HTTP library would quote such a header value unredacted). A `RequestException` is redacted with `_redact` and raised after the `except` block, so the original is not its `__context__`; keep that pattern for any new request path. Caller IDs placed in a URL path go through `_path_segment`.
- **Do not follow redirects.** Every `requests` call passes `allow_redirects=False`. A 3xx response is a `ZBApiException`. A redirect must not be fetched, because the `Location` could be another host and the next request can carry the API key.
- **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.
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Error messages no longer include the raw HTML access-denied page. The live `get_file` page is about 1.4 MB and contains the API key.
- Redirects are not followed, so a response cannot send the client, or the API key, to another host.

## [2.2.4] - 2026-08-14

No changes to the package; CI action updates only.

## [2.2.3] - 2026-06-18

### Fixed
Expand Down Expand Up @@ -152,6 +156,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Initial release: `get_credits`, `get_api_usage`, `get_activity`, `validate`, `validate_batch`, bulk and AI-scoring file methods, and `guess_format`.

[Unreleased]: https://github.com/zerobounce/zero-bounce-python-sdk-setup/compare/v2.2.4...HEAD
[2.2.4]: https://github.com/zerobounce/zero-bounce-python-sdk-setup/compare/v2.2.3...v2.2.4
[2.2.3]: https://github.com/zerobounce/zero-bounce-python-sdk-setup/compare/v2.2.2...v2.2.3
[2.2.2]: https://github.com/zerobounce/zero-bounce-python-sdk-setup/compare/v2.2.0...v2.2.2
[2.2.0]: https://github.com/zerobounce/zero-bounce-python-sdk-setup/compare/v2.1.10...v2.2.0
Expand Down
16 changes: 9 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ By participating in this project, you agree to uphold our [Code of Conduct](CODE

## Getting Started

See the [README](README.md) for prerequisites, setup, and how to run tests.
See the [README](README.md) for setup, linting and tests (`ruff check src tests`, `python -m compileall -q src tests`, `python -m pytest`). [AGENTS.md](AGENTS.md) describes the layout and the conventions changes must follow.

## How to Contribute

Expand All @@ -28,20 +28,22 @@ Open an [issue](https://github.com/zerobounce/zero-bounce-python-sdk-setup/issue

### Submitting Changes

1. **Fork** the repository and create a branch from `main`:
1. **Fork** the repository and create a branch from `master`:
```bash
git checkout -b feature/your-feature-name
```

2. **Make your changes** and add or update tests where relevant.
2. **Make your changes** and add or update tests where relevant. Tests must not call the live API. Only add methods for endpoints documented at https://www.zerobounce.net/docs/.

3. **Run the test suite** (see README) before submitting.
3. **Add a CHANGELOG entry** under `## [Unreleased]` for anything users will notice.

4. **Commit** with a clear message (e.g. `Add X`, `Fix Y`).
4. **Run the lint and test commands** (see README) before submitting.

5. **Push** your branch and open a **Pull Request** against `main`.
5. **Commit** with a [Conventional Commits](https://www.conventionalcommits.org/) message (e.g. `feat: add X`, `fix: Y`, `docs: Z`).

6. In the PR description, briefly explain what changed and why. Link any related issues.
6. **Push** your branch and open a **Pull Request** against `master`.

7. In the PR description, briefly explain what changed and why. Link any related issues.

Maintainers will review and may request changes. Once approved, your PR can be merged.

Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ RUN pip install --no-cache-dir -e ".[test]"

COPY tests/ tests/

# Unit tests only (no API key); use CMD so it can be overridden
# Every offline suite (no API key, no network); use CMD so it can be overridden

USER 65532:65532

CMD ["python", "-m", "pytest", "tests/zero_bounce_test_case.py", "-v", \
CMD ["python", "-m", "pytest", "-v", \
"--cov=zerobouncesdk", "--cov-report=term-missing", "--cov-fail-under=50"]
81 changes: 34 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ More information about ZeroBounce you can find in the [official documentation](h
## Security

- Keep API keys on a trusted server. Do not embed them in mobile apps or browser JavaScript that untrusted users can inspect.
- Custom API base URLs (when supported) must use `https://`. Do not pass end-user-controlled hosts into those settings.
- Request URLs include `api_key` as a query parameter (ZeroBounce API contract). Do not log full request URLs or enable payload debug logging in production.
- Where the key travels, as each API documents it: the validation API takes an `api_key` parameter (in the query string for `GET` requests; in the body with `use_post=True` and for file uploads); the Inbox Placement Tester takes an `Authorization: Bearer` header; the Warmup API takes a separate Warmup key in an `API-Key` header, used only by `ZeroBounceWarmup`. Query strings can end up in proxy and server logs, so do not log full request URLs, and prefer `use_post=True` where a method offers it.
- Every base URL, including the per-API `*_BASE_URL` attributes, must be `https://`: the client refuses to send a request anywhere else. Do not pass end-user-controlled hosts into those settings.
- The client never follows redirects and masks the key in every exception it raises, network errors included. It rejects a key with surrounding whitespace or control characters, such as one read from a file with its trailing newline.

## INSTALLATION
```bash
Expand Down Expand Up @@ -56,6 +57,10 @@ zero_bounce = ZeroBounce("<YOUR_API_KEY>", base_url="https://custom-api.example.
- `ZBApiUrl.API_USA_URL` - USA region API (https://api-us.zerobounce.net/v2/)
- `ZBApiUrl.API_EU_URL` - EU region API (https://api-eu.zerobounce.net/v2/)

The region applies to the single-request API. File-based APIs always use `bulkapi.zerobounce.net` (see [Other hosts](#other-hosts)).

**Errors:** a method raises `ZBClientException` when it rejects an argument before sending anything, and `ZBApiException` when the API returns an error; both inherit from `ZBException`. `ZBApiException.status_code` holds the HTTP status when it is known (for example 429 from the Warmup API). An unknown status, sub-status or confidence value in a response becomes `None` with a `UserWarning` instead of an error.

## Examples
Then you can use any of the SDK methods, for example:

Expand Down Expand Up @@ -103,6 +108,8 @@ except ZBException as e:

* ##### Find the correct email format when you provide a name and email domain or company name

Pass exactly one of `domain` or `company_name`. The name fields are optional, and `use_post=True` sends the request as a form-encoded `POST`.

```python
from zerobouncesdk import ZeroBounce, ZBException

Expand Down Expand Up @@ -374,7 +381,7 @@ file_id="<FILE_ID>" # The returned file ID when calling
local_download_path = "./dwnld_file.csv" # The path where the file will be downloaded

try:
response = zero_bounce.scoring_get_file(file_id, local_download_path)
response = zero_bounce.scoring_get_file(file_id, local_download_path) # stream=True for large files
print("ZeroBounce get_file response: " + str(response))
# Optional third argument: ZBGetFileOptions with download_type only (activity_data is not used for scoring getfile)
# response = zero_bounce.scoring_get_file(file_id, local_download_path, opts)
Expand Down Expand Up @@ -445,7 +452,7 @@ try:
"./people.csv", domain_column=3, first_name_column=1, last_name_column=2, has_header_row=True
)
print(zero_bounce.email_finder_file_status(sent.file_id))
zero_bounce.email_finder_get_file(sent.file_id, "./people_results.csv")
zero_bounce.email_finder_get_file(sent.file_id, "./people_results.csv") # stream=True for large files
zero_bounce.email_finder_delete_file(sent.file_id) # only when the status is Complete

sent = zero_bounce.send_domain_search_file("./domains.csv", domain_column=1, has_header_row=True)
Expand Down Expand Up @@ -575,8 +582,11 @@ zero_bounce.EMAIL_FINDER_BASE_URL = "https://bulkapi.zerobounce.net/email-finder
zero_bounce.DOMAIN_SEARCH_BASE_URL = "https://bulkapi.zerobounce.net/domain-search"
zero_bounce.LIST_EVALUATOR_BASE_URL = "https://bulkapi.zerobounce.net/v2/listevaluator"
zero_bounce.INBOX_PLACEMENT_BASE_URL = "https://bulkapi.zerobounce.net/api/ipt"
zero_bounce.BULK_ROOT_URL = "https://bulkapi.zerobounce.net" # S3 and SFTP/FTP
```

Each of these must be an `https://` URL; the client refuses to send the key anywhere else.

* ##### (Deprecated) Identify the correct email format when you provide a name and email domain

> **鈿狅笍 Deprecated:** The `guess_format` method is deprecated and will be removed in future versions. Use `find_email_format` or `find_domain` instead (see examples above).
Expand All @@ -593,7 +603,7 @@ last_name = "Doe" # The last name of the person whose email format is being sear

try:
response = zero_bounce.guess_format(domain, first_name, middle_name, last_name)
print("ZeroBounce guess format response: " + response)
print("ZeroBounce guess format response: " + str(response))
except ZBException as e:
print("ZeroBounce guess format error: " + str(e))
```
Expand All @@ -602,62 +612,39 @@ except ZBException as e:

### Local setup
```bash
python -m venv venv # python 3.12+
python -m venv venv # Python 3.10+ recommended
source venv/bin/activate
pip install -e .
pip install -e ".[test]"
pip install ruff==0.17.2
```

### Run tests with Docker
From the **`sdk-docs/`** folder in the SDKs monorepo:

### Lint and test
```bash
cd sdk-docs
docker compose build python
docker compose run --rm python
ruff check src tests
python -m compileall -q src tests
python -m pytest
```

Or build and run this SDK鈥檚 image from this directory:
`python -m pytest` runs every offline suite: the scripted unit tests and the `tests/test_*.py` suites, which answer through a fake transport and never call the API. CI runs the same commands, with coverage.

```bash
docker build -t zb-python-sdk .
docker run --rm zb-python-sdk
```
### Live API tests
`tests/zero_bounce_integration_test.py` calls the real API and is not collected by `pytest`. Run it with a valid key:

### Run tests (local)
```bash
python -m tests -v

# output:
python -m tests -v
test_api_regions (tests.zero_bounce_integration_test.ZeroBounceIntegrationTestCase.test_api_regions)
Test that different API regions work. ... skipped 'ZEROBOUNCE_API_KEY environment variable not set'
test_error_handling_invalid_key (tests.zero_bounce_integration_test.ZeroBounceIntegrationTestCase.test_error_handling_invalid_key)
Test error handling with invalid API key. ... skipped 'ZEROBOUNCE_API_KEY environment variable not set'
test_find_domain_with_domain (tests.zero_bounce_integration_test.ZeroBounceIntegrationTestCase.test_find_domain_with_domain)
Test find_domain with domain parameter. ... skipped 'ZEROBOUNCE_API_KEY environment variable not set'

----------------------------------------------------------------------
Ran 4 tests in 0.015s

OK (skipped=10) # integration tests are skipped if no api key is present
export ZEROBOUNCE_API_KEY=<apikey>
python -m unittest tests.zero_bounce_integration_test -v
```

```bash
# to run integration tests export api key into the environment (valid api key required)
export ZEROBOUNCE_API_KEY=<apikey> && python -m tests -v

# output:
test_api_regions (tests.zero_bounce_integration_test.ZeroBounceIntegrationTestCase.test_api_regions)
Test that different API regions work. ... ok
test_error_handling_invalid_key (tests.zero_bounce_integration_test.ZeroBounceIntegrationTestCase.test_error_handling_invalid_key)
Test error handling with invalid API key. ... ok

----------------------------------------------------------------------
Ran 4 tests in 1.1s
Without `ZEROBOUNCE_API_KEY` its tests are skipped. It covers the original single-request methods; the newer APIs are tested offline only.

OK
### Run tests with Docker
```bash
docker build -t zb-python-sdk .
docker run --rm zb-python-sdk
```

From the **`sdk-docs/`** folder in the SDKs monorepo, `docker compose build python` and `docker compose run --rm python` do the same.

## Publish

1. Bump `version` in `pyproject.toml`, commit, tag (`vX.Y.Z`), push tag.
Expand Down
8 changes: 5 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ We focus on the current release line for fixes. Using the latest release is reco

## Tips for Using This SDK

* Don't commit API keys or `.env` files鈥攗se environment variables or a secrets manager.
* Keep dependencies up to date and upgrade when new versions are released.
* The client uses HTTPS by default; avoid overriding to non-HTTPS in production.
* Don't commit API keys or `.env` files鈥攗se environment variables or a secrets manager. The Warmup API uses its own key; keep it separate from the validation API key.
* Keep the SDK and its dependencies up to date. Use Python 3.10 or newer: older versions cannot install the current `requests` and `urllib3` security releases.
* The client only sends requests to `https://` URLs and never follows redirects. Do not point a base URL at a host you don't control.
* The validation API puts the key in the query string of `GET` requests, and query strings can reach proxy and server logs. Don't log full request URLs; use `use_post=True` where a method offers it.
* Exceptions raised by the SDK mask the key, but avoid logging request objects or enabling HTTP debug logging (for example `http.client` debuglevel), which would print the key.

Thanks for helping keep things secure.
Loading
Loading