Skip to content

feat: v2 validate parity (verify_plus, activity_data, batch timeout) - #28

Merged
franciscbalint merged 18 commits into
masterfrom
feature/api-parity-p1-validate
Oct 23, 2026
Merged

franciscbalint merged 18 commits into
masterfrom
feature/api-parity-p1-validate

Conversation

@franciscbalint

@franciscbalint franciscbalint commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Priority 1–2 parity on feature/api-parity-p1-validate (validate options, score_email).

Priority 3 — bulk finder, list evaluator, filters, S3

  • Bulk email finder: send_email_finder_file / _stream, email_finder_file_status, email_finder_get_file, email_finder_delete_file
  • Bulk domain search: send_domain_search_file / _stream, status/get/delete
  • List evaluator: send_list_evaluator_file / _stream, get_list_evaluator_result, send_evaluated_file
  • Filters: add_filter, list_filters(use_post=...), delete_filter
  • S3: list_s3files, validate_s3file, score_s3file, export_s3file on bulkapi root, as documented at https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api

Test plan

  • pytest -k "email_finder or add_filter or list_evaluator" (docker)

Made with Cursor

Validate options, Inbox Placement Tester, list evaluator base URL

  • validate: domain_info (website availability and WHOIS), greylist_processing, callback_url (requires greylist_processing=True). ZBValidateResponse reads domain_website_exists, domain_registrant_company_name, and message while a greylist retry is still running.
  • 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 https://bulkapi.zerobounce.net/api/ipt (base URL and path from the public docs). Arguments are checked before the request: name ≤ 64 characters, test type 1–3, sender_email for type 1, page_size 1–100.
  • Base URLs: LIST_EVALUATOR_BASE_URL and INBOX_PLACEMENT_BASE_URL. The list evaluator lives at a different path on members-backend in test (/api/validation/list-evaluator/), so it can no longer be derived from BULK_BASE_URL. file_id is URL-encoded in the result path.
  • Errors: a detail field (Django REST framework, used by the Inbox Placement Tester) raises ZBApiException with that text instead of raw JSON.
  • Docs: README and documentation.md gain examples for the validate options, score_email, bulk finder and domain search, list evaluator, filters, the Inbox Placement Tester, and the per-API base URLs.

Test plan

  • ruff check src tests, python -m compileall -q src tests, python -m pytest (80 passed): every new endpoint's method, host and path; JSON bodies in camelCase; optional fields omitted until set; invalid arguments send no request; detail errors are short.
  • README examples run against the offline fake transport.
  • Live check of the Inbox Placement Tester and the new validate options with a real key (not run).

S3 and SFTP/FTP bulk

Documented at https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api and https://www.zerobounce.net/docs/api-integrations/sftp_bulk_api.

  • Added: email_finder_s3file, domain_search_s3file, validate_sftpfile, score_sftpfile, email_finder_sftpfile, domain_search_sftpfile, export_sftpfile, sending only the documented fields. has_header_row is required, as on the docs page. validate_sftpfile accepts file_name=None because the docs mark it optional for that endpoint only (likely a docs copy-paste; the other SFTP jobs require it).
  • Fixed against the docs: list_s3files returns the files of the documented {"success": true, "files": [...]} response (it raised before); score_s3file sends return_url; export_s3file's export_bucket is optional.
  • Aligned later in this PR: validate_s3file and score_s3file now take only the documented file_name, has_header_row (required), remove_duplicates and return_url (see below).
  • 82 tests pass (every new endpoint's method/host/path, form fields, omitted optionals, client-side rejections, account errors, the documented listS3files shape). README examples run against the offline transport.

Docs audit fixes and edge tests

An endpoint-by-endpoint audit against https://www.zerobounce.net/docs/ found every documented endpoint covered except the Warmup API (added below), and no SDK method calling an undocumented endpoint.

  • Bugs fixed: find_domain / guess_format raised KeyError on a lowercase message; an unknown confidence raised ValueError; mx_found: "false" (string, as in the docs' samples) read as true; processed_at without fractions and get_api_usage dates in the documented yyyy/mm/dd form raised ValueError; list evaluator uploads showed success=False on success (now ZBListEvaluatorResponse with the documented fields).
  • Added: ai_agent_mailbox sub-status (RD-66, cherry-picked from the unpushed feature/ai-agent-mailbox-substatus); use_post=True on get_credits, get_api_usage, get_activity, find_email_format, find_domain; active_first_seen and two usage counts on the response models.
  • Aligned with the docs: S3 jobs take only documented fields; bulk email finder needs a first-name or full-name column; find_email_format's first_name is optional; filter rule/target and Inbox Placement Tester names are checked before sending.
  • Left as is (released behaviour): scoring_send_file keeps return_url optional and scoring_get_file keeps download_type, though the scoring docs mark the first required and don't list the second.
  • Tests: new tests/test_offline_edges.py: the API key never appears in a transport error on any request path, error bodies (success: false, error_message, Cloudflare, 429, 500, detail), every optional upload field, argument checks, response-model edge cases, the POST variants. 105 tests pass; client coverage 84% → 93%. Every Python example in the README's new sections runs against the offline transport.

Warmup API (ZeroBounceWarmup)

Following https://www.zerobounce.net/docs/warmup-api: a separate client, because the Warmup API has its own team-scoped key, sent in the API-Key header, and its own host (https://warmup-api.zerobounce.net by default, or the base URL ZeroBounce provides).

  • 26 methods, one per documented endpoint: sending accounts (list/get/add/update/delete, change from-address, list from-addresses), engagement (rules catalogue, set standard rule, custom rule get/create/update/delete), statistics and utilities (seeds, seed count, progress/sent today, emails per day, statistics, per-day, detailed, per-address, CSV downloads, DMARC check, SMTP test).
  • Documented behaviour: 204 and empty 200 return None; CSV downloads return text and can save to a path; team_id is filled in where the schema needs it; check_domain_dmarc(domain) sends it as the documented from_address; custom-rule deletion sends a JSON body (query parameters return 422).
  • Checked before sending: onboarding model, rule ids (0–4; 1–4 for the fallback), status ids (0–8), auto_enable required, custom percentages (reply 5–45, important 5–40, click 0–70 and ≤ important × 1.75), YYYY-MM-DD dates and their order, search_from characters, timezone_offset −11…14, email types, SMTP port.
  • Errors: ZBApiException with a new status_code attribute (so callers can back off on 429, including the custom-rule cooldown); {"error"}, {"detail": "Not Found"} and 422 {"detail": [{"loc", "msg"}]} read as short messages; API key and SMTP password masked; redirects not followed.

Python 3.7–3.9

  • ZeroBounce.__init__'s timeout: timedelta | None annotation made import zerobouncesdk raise TypeError on Python 3.7–3.9 (already on master and in 2.2.4; the package declares >=3.7). It is now Optional[timedelta], and the full suite passes on Python 3.9.6.

Tests

  • New tests/test_warmup_offline.py: every endpoint's method, host, path and API-Key header (key never in the URL), request bodies and queries, documented successes (204, empty 200, integer, CSV), 29 invalid-argument cases that send nothing, every documented error status with status_code, redirects and HTML, secret masking. 113 tests pass on 3.12 and 3.9; zb_warmup.py coverage 98%. The README Warmup example runs offline.
  • Not run: the live Warmup API (no Warmup key here).

Security review and dependency scan

Scans: osv-scanner and pip-audit on the resolved environments, bandit on src, an AST check of every requests call (all 7 set timeout and allow_redirects=False, none touches verify), and an independent review of the full diff. No high-severity findings.

  • Fixed: redacted network errors kept the original exception, with the key in its URL or headers, as __context__; keys with whitespace or control characters leaked through the HTTP library's header error; Inbox Placement Tester sent the key in the URL (now the documented Authorization: Bearer header); non-https base URL attributes would have received the key; . / .. path IDs; SMTP credentials on some Warmup error paths; ruff unpinned in CI.
  • Dependencies: requests >= 2.22.0 allowed versions with known advisories. Floors are now the oldest clean releases per Python version (requests / urllib3 / idna / certifi; test extra pytest). Python 3.12 resolves clean in osv-scanner and pip-audit. On Python 3.9 and older the fixed requests 2.33 and urllib3 2.7+ cannot be installed; the README now recommends 3.10+.
  • Tests: tests/test_security_offline.py (7 tests). Reverting the __context__ or key-format fix makes them fail. 120 tests pass on 3.12 and 3.9.
  • Follow-ups done in this PR (they were already on master): opt-in streamed downloads (stream=True); the publish workflow passes the tag through env; Dependabot auto-merge limited to patch/minor with majors left for review; a warning when an EU/US base_url meets the global bulk host. See the section below.
  • Publishing: releases have always gone out through PyPI Trusted Publishing (the project trusts this repository's publish.yml in the release environment; the 2.2.4 run shows no password input and generated attestations). The publish step's password: ${{ secrets.PYPI_API_TOKEN }} referenced a secret that was never set, so it was dead configuration; it is removed and the setup is documented in README and AGENTS.md. Commit 380c50b's message wrongly says a token was replaced and should be revoked; there is none.

Downloads, regional hosts and workflows

  • Streamed downloads: get_file, scoring_get_file, email_finder_get_file and domain_search_get_file take stream=True. The file is written in chunks to a .part file next to download_path and moved into place when complete; the first 64 KB are checked for an error body with the same rules as the buffered path. A dropped connection is redacted and leaves an existing file untouched. Default behaviour is unchanged.
  • EU / US base URL: ZeroBounce documents no regional bulk host (and bulkapi-eu / bulkapi-us don't resolve), so bulk, scoring and finder files, list evaluator, S3/SFTP and Inbox Placement Tester calls go to bulkapi.zerobounce.net. The client now warns once per client when that happens with a regional base_url, unless the *_BASE_URL attributes were set.
  • publish.yml: the tag input reaches the "Verify tag" and "Create release" scripts through env: TAG instead of ${{ }} expansion inside run:, which would execute a crafted value as shell.
  • dependabot-auto-merge.yml: dependabot/fetch-metadata (pinned to v3.1.0's SHA) reads the update type. Patch and minor updates are approved and auto-merged when checks pass; a major update gets a comment and waits for a person. The weekly job only re-queues PRs the workflow already approved.
  • 127 tests on Python 3.12 and 3.9 (7 new: every download method streams, error bodies and redirects leave no file, a dropped connection keeps the previous file and leaks no key, the regional warning appears once and not otherwise).

🤖 Generated with Claude Code

franciscbalint and others added 18 commits October 18, 2026 00:50
Add optional validate and validate_batch parameters and activity fields on validation responses per ZeroBounce v2 docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Mirror JavaScript P3 surfaces: email-finder and domain-search bulk jobs,
list evaluator, allow/block filters, and S3 validate/score/export helpers.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add form-encoded POST option on single validate (alongside GET) and GET
listS3files on bulkapi root, with unit tests and API parity doc updates.
Co-authored-by: Cursor <cursoragent@cursor.com>
HTML access-denied pages and parser exceptions were reaching callers, form flags were sent as True/False, and column 0 was rejected as empty.

Co-authored-by: Cursor <cursoragent@cursor.com>
get_file treated every text/plain body as an error, so a CSV with that content type never saved. Empty downloads still raise. CI runs a narrow Ruff check with the unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
…st evaluator base URL

- validate takes domain_info, greylist_processing and callback_url;
  ZBValidateResponse reads domain_website_exists, domain_registrant_company_name
  and the greylist "still in progress" message.
- Inbox Placement Tester: providers, test types, create/get/list/delete tests,
  get/set webhook on bulkapi.zerobounce.net/api/ipt (INBOX_PLACEMENT_BASE_URL).
- List evaluator requests use LIST_EVALUATOR_BASE_URL, so they can be pointed
  at a host whose path differs from the other bulk endpoints; file_id is
  URL-encoded in the result path.
- Error bodies with a "detail" field (Django REST framework) raise with that text.
- README and documentation.md show the new and earlier parity methods.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
list_s3files, validate_s3file, score_s3file and export_s3file call endpoints
that https://www.zerobounce.net/docs/ does not document. The SDK only covers
documented endpoints (now a rule in AGENTS.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reverts a6710e5. list_s3files, validate_s3file, score_s3file and export_s3file
are documented at https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api,
an Integrations page the earlier check missed. AGENTS.md keeps the rule that the
SDK covers only documented endpoints, and now says integration pages count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the documented endpoints the SDK was missing
(https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api,
https://www.zerobounce.net/docs/api-integrations/sftp_bulk_api):
- email_finder_s3file, domain_search_s3file
- validate_sftpfile, score_sftpfile, email_finder_sftpfile,
  domain_search_sftpfile, export_sftpfile

Each sends only the documented fields (file_name, has_header_row,
remove_duplicates, return_url; file_id and export_path for the export).

Existing S3 methods now match the docs page:
- list_s3files returns the files of the documented {"success", "files"} object
  instead of raising "Unexpected listS3files response".
- score_s3file sends return_url.
- export_s3file's export_bucket is optional (account setting otherwise).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API now returns ai_agent_mailbox under both the valid and catch-all
statuses. No new status was added.

Adds the value to ZBValidateSubStatus following role_based_catch_all, and
keeps the sub-status list in ZBValidateResponse's docstring in sync so it
does not drift from the enum.

Tests cover valid + ai_agent_mailbox and catch-all + ai_agent_mailbox, plus
an explicit check that an unrecognised sub_status still warns and falls back
to None rather than raising — that behaviour already existed via
safe_enum_convert but was not covered.

No sub_status_ai_agent_mailbox usage counter: it is not documented.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
From an endpoint-by-endpoint audit against https://www.zerobounce.net/docs/:

Fixed
- find_domain / guess_format raised KeyError on a lowercase "message" error.
- An unknown confidence value raised ValueError; it is now None with a warning.
- free_email / mx_found / catchall_domain sent as "true"/"false" strings
  (as the docs' samples show mx_found) read as booleans.
- processed_at without fractional seconds and get_api_usage dates in the
  documented yyyy/mm/dd form no longer raise; other formats stay strings.
- List evaluator uploads and results return ZBListEvaluatorResponse with the
  documented fields instead of a ZBSendFileResponse that showed success=False.

Aligned with the docs
- validate_s3file / score_s3file take only the documented file_name,
  has_header_row (required), remove_duplicates and return_url.
- send_email_finder_file needs first_name_column or full_name_column.
- find_email_format's first_name is optional.
- Filter rule/target and Inbox Placement Tester names are checked.
- use_post=True on get_credits, get_api_usage, get_activity,
  find_email_format and find_domain (documented form-encoded POST).
- ZBGetActivityResponse.active_first_seen and the leading_period_removed /
  alias_address usage counts are declared.

Tests (tests/test_offline_edges.py): the API key never appears in a transport
error on any request path; success=false / error_message / Cloudflare / 429 /
500 / detail bodies; every optional upload field; required-argument checks;
response-model edge cases; the POST variants. README examples run offline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds ZeroBounceWarmup for https://www.zerobounce.net/docs/warmup-api, one
method per documented endpoint (26): sending accounts, from-address change and
listing, standard and custom engagement rules, seeds, statistics, CSV
downloads, the DMARC check and the SMTP test.

It is its own client because the Warmup API is: a team-scoped Warmup key sent
in the API-Key header (never in the URL), host https://warmup-api.zerobounce.net
or the base URL ZeroBounce provides, JSON bodies, 204 / empty-200 successes
(returned as None) and CSV downloads. Documented ranges are checked before
sending (rule ids, status ids, custom engagement percentages, dates,
search_from characters, timezone offset, email types). Errors raise
ZBApiException with the new status_code attribute, so callers can back off on
429; the API key and the SMTP password are masked in every message, and
redirects are not followed.

Also:
- Fix: importing the SDK failed on Python 3.7-3.9 (`timedelta | None`
  annotation on ZeroBounce.__init__). The whole suite now passes on 3.9.
- Validation error bodies {"detail": [{"loc", "msg"}]} raise "field: msg".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From a security review and OSV / pip-audit / bandit scans of the PR:

- Network errors were re-raised redacted but still carried the original
  exception, with the API key in its URL or headers, as __context__. The
  redacted copy is now raised outside the except block (all 7 request paths,
  both clients).
- ZeroBounce and ZeroBounceWarmup reject keys with surrounding whitespace or
  control characters: as a header value the HTTP library raised an error that
  quoted the key unredacted (e.g. a key read from a file with its newline).
  Redaction also masks the escaped (repr) form.
- Inbox Placement Tester calls send the key in the documented
  Authorization: Bearer header instead of the URL.
- Requests to any non-https base URL are refused before sending, covering the
  reassignable per-API base URL attributes.
- "." and ".." are rejected as URL path IDs.
- ZeroBounceWarmup masks the SMTP username/password on every error path.
- Dependency floors raised to the oldest releases without OSV advisories,
  per Python version (requests, urllib3, idna, certifi); test extra pytest.
  The resolved Python 3.12 environment scans clean with osv-scanner and
  pip-audit.
- CI pins ruff (0.17.2).

New tests/test_security_offline.py checks each property; reverting the
__context__ or key-format fix makes it fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… hardening

- get_file, scoring_get_file, email_finder_get_file and domain_search_get_file
  take stream=True: the result is written in chunks to a temporary file next to
  download_path and moved into place when complete, so a large file never has to
  fit in memory and a failed download leaves no partial file (an existing file is
  kept). The first 64 KB are checked for an error body with the same rules as
  before. Without the flag nothing changes.
- With an EU or US base_url, a request to bulkapi.zerobounce.net warns once per
  client (UserWarning): ZeroBounce documents no regional bulk host (none exists in
  DNS either), so bulk, scoring/finder files, list evaluator, S3/SFTP and Inbox
  Placement Tester data goes to the global host unless the *_BASE_URL attributes
  are set.
- publish.yml: the tag input reaches the two remaining run: steps through env
  instead of being expanded into the script.
- dependabot-auto-merge.yml: only patch and minor updates (dependabot/fetch-metadata,
  pinned) are approved and auto-merged; a major update gets a comment and waits
  for a person. The weekly job only re-queues PRs the workflow already approved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The zerobouncesdk project on PyPI now trusts this repository's publish.yml in
the "release" environment, so pypa/gh-action-pypi-publish exchanges the job's
OIDC token (id-token: write, already granted) for a short-lived upload token.
The PYPI_API_TOKEN secret is no longer used; delete it and revoke the token on
PyPI after the first release succeeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Correction to 380c50b: there was never a PyPI API token. The publish step's
`password: ${{ secrets.PYPI_API_TOKEN }}` referenced a secret that was never
set, so it expanded to nothing and pypa/gh-action-pypi-publish used Trusted
Publishing for every release (the 2.2.4 run shows no password input and
generated attestations, which PyPI only accepts from Trusted Publishing).
Removing that line changed nothing at publish time; there is nothing to revoke.

README's Publish section, AGENTS.md's release steps and the workflow comment
now describe the setup: the trusted publisher (this repository, publish.yml,
environment "release"), the OIDC exchange, and that renaming the workflow or
environment needs the PyPI publisher updated first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@franciscbalint
franciscbalint merged commit 4d3d322 into master Oct 23, 2026
6 checks passed
@franciscbalint
franciscbalint deleted the feature/api-parity-p1-validate branch October 23, 2026 22:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant