Repository navigation
feat: v2 validate parity (verify_plus, activity_data, batch timeout) - #28
Merged
Merged
Conversation
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>
3 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Priority 1–2 parity on
feature/api-parity-p1-validate(validate options,score_email).Priority 3 — bulk finder, list evaluator, filters, S3
send_email_finder_file/_stream,email_finder_file_status,email_finder_get_file,email_finder_delete_filesend_domain_search_file/_stream, status/get/deletesend_list_evaluator_file/_stream,get_list_evaluator_result,send_evaluated_fileadd_filter,list_filters(use_post=...),delete_filterlist_s3files,validate_s3file,score_s3file,export_s3fileon bulkapi root, as documented at https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-apiTest 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(requiresgreylist_processing=True).ZBValidateResponsereadsdomain_website_exists,domain_registrant_company_name, andmessagewhile a greylist retry is still running.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_webhookonhttps://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_emailfor type 1,page_size1–100.LIST_EVALUATOR_BASE_URLandINBOX_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 fromBULK_BASE_URL.file_idis URL-encoded in the result path.detailfield (Django REST framework, used by the Inbox Placement Tester) raisesZBApiExceptionwith that text instead of raw JSON.documentation.mdgain 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;detailerrors are short.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.
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_rowis required, as on the docs page.validate_sftpfileacceptsfile_name=Nonebecause the docs mark it optional for that endpoint only (likely a docs copy-paste; the other SFTP jobs require it).list_s3filesreturns thefilesof the documented{"success": true, "files": [...]}response (it raised before);score_s3filesendsreturn_url;export_s3file'sexport_bucketis optional.validate_s3fileandscore_s3filenow take only the documentedfile_name,has_header_row(required),remove_duplicatesandreturn_url(see below).listS3filesshape). 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.
find_domain/guess_formatraisedKeyErroron a lowercasemessage; an unknown confidence raisedValueError;mx_found: "false"(string, as in the docs' samples) read as true;processed_atwithout fractions andget_api_usagedates in the documentedyyyy/mm/ddform raisedValueError; list evaluator uploads showedsuccess=Falseon success (nowZBListEvaluatorResponsewith the documented fields).ai_agent_mailboxsub-status (RD-66, cherry-picked from the unpushedfeature/ai-agent-mailbox-substatus);use_post=Trueonget_credits,get_api_usage,get_activity,find_email_format,find_domain;active_first_seenand two usage counts on the response models.find_email_format'sfirst_nameis optional; filterrule/targetand Inbox Placement Tester names are checked before sending.scoring_send_filekeepsreturn_urloptional andscoring_get_filekeepsdownload_type, though the scoring docs mark the first required and don't list the second.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-Keyheader, and its own host (https://warmup-api.zerobounce.netby default, or the base URL ZeroBounce provides).None; CSV downloads return text and can save to a path;team_idis filled in where the schema needs it;check_domain_dmarc(domain)sends it as the documentedfrom_address; custom-rule deletion sends a JSON body (query parameters return 422).auto_enablerequired, custom percentages (reply 5–45, important 5–40, click 0–70 and ≤ important × 1.75),YYYY-MM-DDdates and their order,search_fromcharacters,timezone_offset−11…14, email types, SMTP port.ZBApiExceptionwith a newstatus_codeattribute (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__'stimeout: timedelta | Noneannotation madeimport zerobouncesdkraiseTypeErroron Python 3.7–3.9 (already on master and in 2.2.4; the package declares>=3.7). It is nowOptional[timedelta], and the full suite passes on Python 3.9.6.Tests
tests/test_warmup_offline.py: every endpoint's method, host, path andAPI-Keyheader (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 withstatus_code, redirects and HTML, secret masking. 113 tests pass on 3.12 and 3.9;zb_warmup.pycoverage 98%. The README Warmup example runs offline.Security review and dependency scan
Scans:
osv-scannerandpip-auditon the resolved environments,banditonsrc, an AST check of everyrequestscall (all 7 settimeoutandallow_redirects=False, none touchesverify), and an independent review of the full diff. No high-severity findings.__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 documentedAuthorization: Bearerheader); non-https base URL attributes would have received the key;./..path IDs; SMTP credentials on some Warmup error paths;ruffunpinned in CI.requests >= 2.22.0allowed versions with known advisories. Floors are now the oldest clean releases per Python version (requests/urllib3/idna/certifi; test extrapytest). Python 3.12 resolves clean in osv-scanner and pip-audit. On Python 3.9 and older the fixedrequests2.33 andurllib32.7+ cannot be installed; the README now recommends 3.10+.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.stream=True); the publish workflow passes the tag throughenv; Dependabot auto-merge limited to patch/minor with majors left for review; a warning when an EU/USbase_urlmeets the global bulk host. See the section below.publish.ymlin thereleaseenvironment; the 2.2.4 run shows no password input and generated attestations). The publish step'spassword: ${{ 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
get_file,scoring_get_file,email_finder_get_fileanddomain_search_get_filetakestream=True. The file is written in chunks to a.partfile next todownload_pathand 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.bulkapi-eu/bulkapi-usdon't resolve), so bulk, scoring and finder files, list evaluator, S3/SFTP and Inbox Placement Tester calls go tobulkapi.zerobounce.net. The client now warns once per client when that happens with a regionalbase_url, unless the*_BASE_URLattributes were set.env: TAGinstead of${{ }}expansion insiderun:, which would execute a crafted value as shell.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.🤖 Generated with Claude Code