Skip to content

feat(validate): Priority 1 v2 parity (options + activity fields) - #59

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

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

Conversation

@franciscbalint

@franciscbalint franciscbalint commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Add ZBValidateOptions (verify_plus, activity_data, timeout 3–60 / batch 10–120) on validate and validatebatch
  • Map active_in_days and active_first_seen on ZBValidateResponse

Priority 2 — real-time /v2/scoring

  • Add ZBScoreEmailResponse (email, score)
  • scoreEmail — GET api_key + email query params on main apiBaseUrl
  • scoreEmailPost — POST form-encoded api_key + email

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

  • Bulk email finder / domain search (bulkapi /email-finder, /domain-search): sendfile, filestatus, getfile, deletefile (+ stream uploads)
  • List evaluator: sendListEvaluatorFile, getListEvaluatorResult, sendEvaluatedFile
  • Filters: addFilter, listFilters (GET or POST), deleteFilter
  • S3: validateS3File, scoreS3File, exportS3File
  • Options types: ZBEmailFinderSendFileOptions, ZBDomainSearchSendFileOptions, ZBValidateS3FileOptions, ZBScoreS3FileOptions

Test plan

  • ./gradlew :zero_bounce_sdk:testDebugUnitTest --tests "com.zerobounce.android.ZeroBounceSDKTest.validateEmail*"
  • ./gradlew :zero_bounce_sdk:testDebugUnitTest --tests "com.zerobounce.android.ZeroBounceSDKTest.scoreEmail*"
  • ./gradlew :zero_bounce_sdk:testDebugUnitTest (includes P3: sendEmailFinderFile, validateS3File, addFilter)

Made with Cursor


Validate WHOIS and greylist options, Inbox Placement Tester, SFTP bulk

Ports 407f0a8 and b0444f8 from the Python SDK, on top of the work above. Everything is documented at https://www.zerobounce.net/docs/.

Added

  • ZBValidateOptions gains domainInfo, greylistProcessing and callbackUrl. Booleans go on the wire as "true"/"false" on both the GET and the form POST. callbackUrl is where the greylist retry posts its result, so it is rejected without greylistProcessing, and when blank, before any request is sent.
  • ZBValidateResponse reads domain_website_exists, domain_registrant_company_name and Message. Message is the API's Message key, returned instead of a result while a greylist retry is still running.
  • emailFinderS3File, domainSearchS3File — https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api
  • validateSFTPFile, scoreSFTPFile, emailFinderSFTPFile, domainSearchSFTPFile, exportSFTPFile — https://www.zerobounce.net/docs/api-integrations/sftp_bulk_api
  • The Inbox Placement Tester. The API key goes on the query string, bodies are camelCase JSON, unset fields are left out, and it needs PUT and DELETE, so priority3JsonRequest builds the request itself rather than reusing the form-post helpers.
  • setListEvaluatorBaseUrl and setInboxPlacementBaseUrl, with matching state on SdkState and wrappers on ZeroBounceClient.

Changed

  • The list evaluator uses its own base URL instead of building the path from the bulk base. sendEvaluatedFile stays on the bulk base.
  • exportS3File's exportBucket is nullable, matching the documented endpoint: null uses the export bucket from the account's settings. A blank string is still rejected.

Fixed — a real bug the new tests caught
A file id and an Inbox Placement test id are now encoded as a single path segment. Both went through enc(), which is URLEncoder form encoding and turns a space into + — correct in a query string, but a literal plus in a path. For the id a b/c the list evaluator was sending /v2/listevaluator/a+b%2Fc/. encPathSegment is new.

The two bulk shapes that are easy to get wrong

  • validateSFTPFile's file_name is optional — leaving fileName null uses the input bucket from the account's settings. Every other job requires it, and a blank name is still rejected.
  • exportSFTPFile sends export_path, not the export_bucket that exportS3File sends.

The SFTP server, path and protocol are never sent by the SDK; they come from the account's ZeroBounce settings.

Test plan

  • ./gradlew :zero_bounce_sdk:testDebugUnitTest — 81 pass, up from 72 (9 new in OfflineApiTest)
  • Offline only: a fake OkHttp interceptor records every call and answers without the network

🤖 Generated with Claude Code


RD-66: ai_agent_mailbox sub_status

Folded in from the former feature/ai-agent-mailbox-substatus branch, which has been deleted now that the commit lives here.

Adds the ai_agent_mailbox sub_status, which the API returns alongside both valid and catch-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_mailbox usage counter was added: it is not documented.


Python-SDK parity, verified against the live API

Brings this SDK level with the Python SDK's master, then checks every call against the real API. 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 a real defect, found by calling the endpoint:

  • 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" — which looks like a billing problem, not a payload one.
  • The Inbox Placement Tester authenticates with Authorization: Bearer, not an api_key parameter — the key no longer reaches a query string that gets logged — and its listing is GET /tests. GET /test answers Method "GET" not allowed, so listInboxPlacementTests had never worked.
  • sendEvaluatedFile takes the list evaluator's file_id as a form field. A file upload is rejected with file_id: This field is required, so it now takes a file id rather than a File.
  • The S3 and SFTP jobs require file_name and return_url — validateSFTPFile included, although the docs list file_name as optional for that one.

Two bugs the live run exposed in this SDK.

  • Every fileStatus call failed. Bulk filestatus answers with an ISO-8601 upload_date (2026-10-24T05:24:13Z), and GsonDateDeserializer only knew yyyy-MM-dd HH:mm:ss.SSS and MM/dd/yyyy h:mm:ss a. The parse threw, the whole response was reported as an error, and the caller got the record's own field values as the error message. The deserializer now accepts the ISO-8601 forms too, and an offline test pins each one.
  • {"success": false} on an HTTP 200 was returned as data. The API answers an unknown bulk file id that way. It now reaches the error callback, and ErrorResponse.statusCode carries the HTTP status so callers can tell a Cloudflare 403 from an API 400/404 and a Warmup 429.

New: the Warmup API client. ZeroBounceWarmup covers all 26 documented endpoints of what is a separate product — its own host, its own team-scoped key in an API-Key header, 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 as null, checkDomainDmarc requires an email address (the live API rejects a bare domain), and the SMTP test masks the credentials out of every error it raises. A payload that is not a JSON object comes back as the JsonElement it is, because /seeds answers with an array and /seed-count with a bare number.

Testing. 117 offline tests pass (gradle :zero_bounce_sdk:testDebugUnitTest), no network. The live suite is excluded from the default test task unless -PzbLive is passed, so CI never runs it; the last full run with ZEROBOUNCE_LIVE_SPEND=1 ZEROBOUNCE_LIVE_CHANGES=1 was 40 passed, 0 failed, 4 skipped (the four skips are Warmup statistics endpoints, which need a sending account this team does not have). 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; the filter and sending account it does create are deleted in cleanup even when a check fails.

No version bump, tag or publish.

🤖 Generated with Claude Code

franciscbalint and others added 12 commits October 18, 2026 00:59
Expose verify_plus, activity_data, and timeout on validate/validatebatch and map active_in_days/active_first_seen on responses.

Co-authored-by: Cursor <cursoragent@cursor.com>
Expose /v2/scoring on the main API with form-encoded POST parity and MockWebServer tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
Align Android SDK with Java/JS Priority 3 surface on bulkapi endpoints.

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.
HTML access-denied pages and empty bodies were reaching callers, and an account-level validateBatch error was treated as success. ZeroBounceClient keeps one API key, and the offline suite covers the public methods without the network.

Co-authored-by: Cursor <cursoragent@cursor.com>
…TP bulk

Ports 407f0a8 and b0444f8 from the Python SDK, on top of the parity work
already on this branch.

Added:
- ZBValidateOptions gains domainInfo, greylistProcessing and callbackUrl.
  Booleans go on the wire as the strings true/false on both the GET and the
  form POST. callbackUrl is where the greylist retry posts its result, so it
  is rejected without greylistProcessing, and when blank, before any request
  is sent.
- ZBValidateResponse reads domain_website_exists,
  domain_registrant_company_name and Message. Message is the API's "Message"
  key, returned instead of a result while a greylist retry is still running.
- emailFinderS3File and domainSearchS3File, from
  https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api
- validateSFTPFile, scoreSFTPFile, emailFinderSFTPFile, domainSearchSFTPFile
  and exportSFTPFile, from
  https://www.zerobounce.net/docs/api-integrations/sftp_bulk_api
- The Inbox Placement Tester. The API key goes on the query string, bodies are
  camelCase JSON, unset fields are left out, and it needs PUT and DELETE, so
  priority3JsonRequest builds the request itself rather than reusing the
  form-post helpers.
- setListEvaluatorBaseUrl and setInboxPlacementBaseUrl, with matching state on
  SdkState and wrappers on ZeroBounceClient.

Changed:
- The list evaluator uses its own base URL instead of building the path from
  the bulk base. sendEvaluatedFile stays on the bulk base.
- exportS3File's exportBucket is nullable, matching the documented endpoint:
  null uses the export bucket from the account's settings. A blank string is
  still rejected.

Fixed:
- A file id and an Inbox Placement test id are encoded as a single path
  segment. Both went through enc(), which is URLEncoder form encoding and
  turns a space into "+" — correct in a query string, but a literal plus in a
  path. The two new encoding tests caught this: the list evaluator was sending
  /v2/listevaluator/a+b%2Fc/ for the id "a b/c". encPathSegment is new.

The six S3 / SFTP file jobs share ZBBulkFileJobOptions and bulkRootFileJob,
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 rest and are easy to get wrong:
- validateSFTPFile's file_name is optional; leaving fileName null uses the
  input bucket from the account's settings. Every other job requires it, and a
  blank name is still rejected.
- exportSFTPFile sends export_path, not the export_bucket exportS3File sends.

Tests: 81 pass, up from 72. Offline only — a fake OkHttp interceptor records
every call and answers without the network.

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 AI_AGENT_MAILBOX to ZBValidateSubStatus with its @SerializedName,
following ROLE_BASED_CATCH_ALL.

Unknown values were already safe: Gson maps an unrecognised enum constant to
null rather than throwing, so a sub_status added after this SDK shipped
degrades to null instead of failing the validate call. That had no coverage,
so a test now pins it alongside the two new ones.

Verified: 42 tests, 0 failures.

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 sends 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 an Authorization: Bearer
  header, not an api_key parameter, so the key no longer reaches a query
  string that gets logged. Its listing is GET /tests; /test is the
  single-test path and answers Method "GET" not allowed.
- sendEvaluatedFile posts the list evaluator's file_id as a form field,
  not a file upload, so it now takes a file id rather than a File.
- 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. ErrorResponse now carries statusCode, 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.
ZeroBounceSDK stays the validation client; ZeroBounceWarmup covers all
26 documented endpoints, including the two CSV downloads.

Two live findings are recorded at the call site: checkDomainDmarc takes
an email address, not the domain the docs describe, and a failed SMTP
login comes back as an HTTP 500. The key is masked out of every message,
and the SMTP username and password out of errors from that method.

A payload that is not a JSON object comes back as the JsonElement it is:
/seeds answers with an array and /seed-count with a bare number. A body
that is not JSON at all is rejected before Gson's lenient parser can
read it as a string.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LiveApiTest calls every SDK method against the real API. It is a unit
test so it can reuse the test dependencies, but the default test task
excludes it unless -PzbLive is passed, so `gradle test` and CI never run
it.

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 a check fails. Nothing the API cannot undo is ever called.

The bug: every fileStatus call failed. Bulk filestatus answers with an
ISO-8601 upload_date, for example 2026-10-24T05:24:13Z, and
GsonDateDeserializer only knew "yyyy-MM-dd HH:mm:ss.SSS" and
"MM/dd/yyyy h:mm:ss a". The parse threw, the whole response was reported
as an error, and the caller got the record's field values as an error
message. The deserializer now accepts the ISO-8601 forms too, and an
offline test pins each one.

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, and its S3/SFTP examples now pass the fileName and returnUrl the
API requires. 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 auditing every SDK's api-usage model against the keys the live API
actually returns, after the same class of bug turned up in Rust and iOS.

sub_status_leading_period_removed and sub_status_alias_address are returned by
getapiusage and were not read at all, so both were silently discarded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@franciscbalint
franciscbalint merged commit ee85fd3 into master Oct 24, 2026
7 checks passed
@franciscbalint
franciscbalint deleted the feature/api-parity-p1-validate branch October 24, 2026 19:07
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