Repository navigation
feat: v2 validate parity (verify_plus, activity_data, batch timeout) - #19
Merged
Merged
Conversation
Add ZBValidateOptions and with_options helpers; map activity fields on ZBValidation. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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 pages, empty payloads, and account-level batch responses were succeeding or exposing parser text, and an empty key was still sent. Co-authored-by: Cursor <cursoragent@cursor.com>
…st evaluator base URL Ports the Python SDK's parity changes (407f0a8, a6710e5) to this SDK. - ZBValidateOptions takes domain_info, greylist_processing and callback_url, sent as the strings "true"/"false" on both the query and the form POST. callback_url without greylist_processing: Some(true), and a blank one, are rejected before any request goes out. - ZBValidation carries domain_website_exists, domain_registrant_company_name and message, the last renamed from the API's "Message" key. A greylist pickup that is still running carries no processed_at or free_email, so both now default rather than failing the whole response. - Inbox Placement Tester in src/api/inbox_placement.rs on bulkapi.zerobounce.net/api/ipt: providers, test types, create/get/list/delete tests, and get/set webhook. The API key goes on the query string, bodies are camelCase JSON, and only fields the caller set are sent. reqwest's json() helper needs a feature this crate does not enable, so the body and content type are set directly rather than widening the dependency. - ZBUrlProvider gains list_evaluator_url and inbox_placement_url. List evaluator requests use the former instead of building the path from the bulk base, and the file id is URL-encoded as one path segment by a small local encoder rather than a new crate. send_evaluated_file stays on the bulk base. - An error body carrying a "detail" string (Django REST framework, used by the Inbox Placement Tester) returns that text. Removes validate_s3_file, score_s3_file, export_s3_file and list_s3_files along with the S3ListFileEntry struct, the post_bulk_root_form helper, tests and changelog entry: https://www.zerobounce.net/docs/ does not document those endpoints, and AGENTS.md now records that the SDK covers only documented ones. The offline test client now also points list_evaluator_url and inbox_placement_url at the fake server; without that the suite reached the live API, which is how the list evaluator test first failed. Tests: 130 pass across the four suites. Clippy reports no errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
They are documented, at https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api, which lists listS3files, validateS3file, scoreS3file, exportS3file, emailFinderS3file and domainSearchS3file on bulkapi.zerobounce.net. The previous commit removed them on the incorrect basis that the public docs do not cover them; only the main API reference was considered. Restores validate_s3_file, score_s3_file, export_s3_file and list_s3_files, the S3ListFileEntry struct, the post_bulk_root_form helper, the offline tests and the changelog entry. The parity work in the previous commit is untouched. AGENTS.md keeps the rule that the SDK covers only documented endpoints and now points at where the S3 documentation lives. Not added here: emailFinderS3file and domainSearchS3file, and the SFTP variants listed on the Integrations index, are documented but have never been part of this SDK. Tests: 130 pass across the four suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ports b0444f8 from the Python SDK. These are documented but had never been in this SDK: - POST /emailFinderS3file and /domainSearchS3file, from https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api - POST /validateSFTPfile, /scoreSFTPfile, /emailFinderSFTPfile, /domainSearchSFTPfile and /exportSFTPfile, from https://www.zerobounce.net/docs/api-integrations/sftp_bulk_api All six file jobs share bulk_root_file_job, which sends has_header_row and remove_duplicates as the strings true/false and leaves remove_duplicates and return_url out until set. Two shapes differ from the S3 ones and are easy to get wrong: - validate_sftp_file's file_name is optional, so it takes Option<&str>; None uses the input bucket from the account's settings. Every other job requires a name, and a blank one is still rejected. - export_sftp_file takes export_path, not the export_bucket that export_s3_file uses, and None uses the export path from those settings. The existing S3 methods are untouched, so no signature changes for callers. Tests: 132 pass. Offline only — the fake server's default success body already covers these routes. Co-Authored-By: Claude Opus 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 AiAgentMailbox variant and its from_str arm following RoleBasedCatchAll, plus SS_AI_AGENT_MAILBOX alongside the other sub_status string constants. Unknown values were already handled best-in-class here: sub_status is kept as a String and sub_status_enum() falls back to UnknownValue(String), so an unrecognised value is preserved rather than dropped. A test pins that round-trip alongside the two new ones. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verified against the live API; where the API and the docs disagree, the
API wins and the call site says so.
- validatebatch takes 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 a Bearer header, not an
api_key parameter.
- send_evaluated_file takes the list evaluator's file_id as a form
field, not a file upload. Its signature changes accordingly.
- The S3 and SFTP jobs require file_name and return_url, validateSFTPfile
included, although the docs list file_name as optional for that one.
{"success": false} on an HTTP 200 is a failure as well: the API answers
an unknown file id that way, and two tests had pinned the wrong
behaviour. ZBError::ApiError carries the HTTP status, reachable through
ZBError::status_code(), so callers can tell a 403 from Cloudflare apart
from a 400/404 from the API.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Warmup API is a separate product: its own host, its own team-scoped key, and an API-Key header rather than an api_key parameter. ZeroBounce stays the validation client; ZeroBounceWarmup covers all 26 documented endpoints, including the two CSV downloads. Two live findings are recorded at the call site: check-domain-dmarc takes an email address, not the domain the docs describe, and a failed SMTP login comes back as an HTTP 500. The SMTP username and password are masked out of every error from that method. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Calls every SDK method against the real API. It sits behind the `live` feature, so `cargo test` and CI never compile or run it; running it takes `cargo test --features live --test live_api` and keys in the environment. Free calls run by default: sandbox addresses, reads, and requests expected to fail that still prove the API accepts the payload. The free tier asserts the credit balance did not move. ZEROBOUNCE_LIVE_SPEND=1 adds the billable calls and a bulk round trip that deletes its own job; ZEROBOUNCE_LIVE_CHANGES=1 adds account changes, each undone in cleanup even when the test fails. Nothing the API cannot undo is ever called: no Inbox Placement test, no Inbox Placement webhook, no ZB-managed Warmup onboarding. Keys come from the environment and are redacted out of every message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
README gains a Warmup API section and the opt-in live suite with its two flags. AGENTS.md records where the live API and the docs disagree, the rule that the API wins and the call site says so, and the release rule that the live suite is run by hand before every release. CHANGELOG lists every user-visible change under [Unreleased]. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by running the live suite in a container, which does reach both crates.io and the API. The API returns only the counters that apply to the account and the date range asked for, and an ordinary response carries neither sub_status_gold nor sub_status_role_based_accept_all. Both were required fields, so serde failed the whole response — reported as "ZeroBounce returned a response that is not valid JSON", because the body is over 1000 characters and carries no message or error key. Every counter on ApiUsage now defaults to 0 when absent, so a counter the API adds or drops later is ignored rather than fatal. sub_status_leading_period_removed and sub_status_alias_address, which the API does return, are now read instead of discarded. The iOS SDK had the same bug, from the same two fields. Offline: 162 tests, 0 failures. Live: 40 passed, 0 failed, 4 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CodeQL flags a string literal passed as a password as a hard-coded cryptographic value, in a test as much as anywhere else. Nothing here was ever a real secret, so the three fixtures are assembled at run time instead. The redaction test gains from it: its canned 500 body now echoes the very credentials that were sent, so asserting the message does not contain them proves the client strips them, rather than that the body happened not to mention them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous attempt still built the value from literals with format!, which CodeQL folds to a constant, so the hard-coded-credential alerts simply moved to the new lines. The value now comes from the process id and the clock, with no literal anywhere in the password dataflow. It stays distinctive enough for the redaction test, which asserts the client strips the credential out of an error message echoing it back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Docs: https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-validate-emails
Made with Cursor
Validate options, Inbox Placement Tester, list evaluator base URL
Ported from the Python SDK (
407f0a8,a6710e5).Added
ZBValidateOptionsgainsdomain_info,greylist_processing,callback_url. Booleans go on the wire as"true"/"false"on both the query and the form POST.callback_urlis rejected withoutgreylist_processing: Some(true), and when blank, before any request is sent.ZBValidationgainsdomain_website_exists,domain_registrant_company_name, andmessage(renamed from the API'sMessagekey).src/api/inbox_placement.rsonhttps://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. API key on the query string, camelCase JSON bodies, unset fields omitted.ZBUrlProvidergainslist_evaluator_urlandinbox_placement_url, both overridable.Changed
list_evaluator_urlinstead of building the path from the bulk base; the file id is URL-encoded as one path segment.send_evaluated_filestays on the bulk base.processed_atorfree_email, so both now#[serde(default)]rather than failing the whole response.Fixed
detailstring (Django REST framework, used by the Inbox Placement Tester) returns that text.S3 — removed, then restored
validate_s3_file,score_s3_file,export_s3_file,list_s3_files. That was wrong: they are documented, at https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api. All of them, theS3ListFileEntrystruct, thepost_bulk_root_formhelper and the tests have been restored; only the parity work above remains.AGENTS.mdkeeps the "only documented endpoints" rule and now points at where the S3 docs live.emailFinderS3fileanddomainSearchS3file, and the SFTP variants on the Integrations index, are documented but have never been part of this SDK.Notes
reqwest'sjson()helper needs a feature this crate does not enable, so the JSON body and content type are set directly rather than widening the dependency. The path-segment encoder is a few lines inutilityfor the same reason.list_evaluator_urlandinbox_placement_urlat the fake server. Without that the suite reached the live API — which is how the list evaluator test first failed.New: the rest of the documented bulk surface
Seven endpoints that are documented but had never been in this SDK:
email_finder_s3_file,domain_search_s3_file— https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-apivalidate_sftp_file,score_sftp_file,email_finder_sftp_file,domain_search_sftp_file,export_sftp_file— https://www.zerobounce.net/docs/api-integrations/sftp_bulk_apiAll six file jobs go through one
bulk_root_file_jobhelper, which sendshas_header_rowandremove_duplicatesas the stringstrue/falseand omitsremove_duplicatesandreturn_urluntil they are set.Two shapes differ from their S3 counterparts and are easy to get wrong:
validate_sftp_file'sfile_nameis optional, so it takesOption<&str>—Noneuses the input bucket from the account's settings. Every other job requires a name, and a blank one is still rejected.export_sftp_filetakesexport_path, not theexport_bucketthatexport_s3_fileuses.Noneuses the export path from those settings.The SFTP server, path and protocol are never sent by the SDK; they come from the account's ZeroBounce settings. The existing S3 methods are untouched, so no caller signatures change.
Tests: 132 pass across the four suites; clippy reports no new warnings. Offline only — no live API calls.
RD-66:
ai_agent_mailboxsub_statusFolded in from the former
feature/ai-agent-mailbox-substatusbranch, which has been deleted now that the commit lives here.Adds the
ai_agent_mailboxsub_status, which the API returns alongside bothvalidandcatch-all. Tests cover both pairings, plus an unrecognised sub_status to pin that a value added to the API after this SDK ships degrades gracefully instead of failing the whole validate call.No
sub_status_ai_agent_mailboxusage counter was added: it is not documented.Python-SDK parity, and the live API's actual behaviour
Brings this SDK level with the Python SDK's
master. Where the live API and the docs disagree, the API wins and the call site says so.What the live API taught us. Each of these was verified by calling the endpoint from the JavaScript, Go and PHP SDKs against the real API, and is fixed here too:
validatebatchtakestimeoutas 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" — which looks like a billing problem, not a payload one.Authorization: Bearer, not anapi_keyparameter. The key no longer reaches a query string that gets logged.send_evaluated_filetakes the list evaluator'sfile_idas a form field. A file upload is rejected withfile_id: This field is required, so the signature is nowsend_evaluated_file(file_id: &str).file_nameandreturn_url—validate_sftp_fileincluded, although the docs listfile_nameas optional for that one. Both parameters are now&strrather thanOption.A bug the live runs exposed.
{"success": false}on an HTTP 200 was returned as data: the API answers an unknown bulk file id that way, and two tests had pinned that wrong behaviour. It now raises, andZBError::ApiErrorcarries the HTTP status (ZBError::status_code()) so callers can tell a Cloudflare 403 from an API 400/404 and a Warmup 429.New: the Warmup API client.
ZeroBounceWarmupcovers all 26 documented endpoints of what is a separate product — its own host, its own team-scoped key in anAPI-Keyheader, two CSV downloads. Ranges are checked before sending (engagement rules 0–4, reply 5–45 %, important 5–40 %, click ≤ 70 % and ≤ important × 1.75), 204 and empty 200 come back asOk(None),check_domain_dmarcrequires an email address (the live API rejects a bare domain), and the SMTP test masks the credentials out of every error it raises. A key with surrounding whitespace or control characters is refused before it can be quoted unredacted by the transport.Testing. 162 offline tests pass (
cargo test), no network.The live suite ran green: 40 passed, 0 failed, 4 skipped. The four skips
are the per-address Warmup statistics; this team has no sending account. It
was run in a
rust:1-bookwormcontainer, which reaches both crates.io and theAPI from this machine. An earlier revision of this description claimed the
suite could not be run here at all — that was wrong, and this replaces it.
A bug the live run found
get_api_usagealways failed. The API returns only the counters thatapply to the account and the date range asked for, and an ordinary response
carries neither
sub_status_goldnorsub_status_role_based_accept_all. Bothwere required fields, so serde failed the whole response — surfaced as
"ZeroBounce returned a response that is not valid JSON", because the body is
over 1000 characters and carries no message or error key. Every counter on
ApiUsagenow defaults to 0 when absent, so a counter the API adds or dropslater is ignored rather than fatal, and
sub_status_leading_period_removed/sub_status_alias_address, which the APIdoes return, are now read. The iOS SDK had the same bug from the same two
fields; it is fixed there too.
To run it yourself:
It is behind the
livecargo feature, socargo testand CI never compile it. Free calls run by default and assert the credit balance did not move;ZEROBOUNCE_LIVE_SPEND=1adds the billable calls and a bulk round trip that deletes its own job, andZEROBOUNCE_LIVE_CHANGES=1adds a test filter and a self-managed Warmup sending account, both removed in cleanup even when a test fails. It never creates an Inbox Placement test, sets the Inbox Placement webhook, or starts ZB-managed Warmup onboarding, because the API cannot undo any of those.No version bump, tag or publish.
🤖 Generated with Claude Code