Skip to content

feat(validate): Priority 1 v2 parity (ZBValidateOptions) - #17

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

franciscbalint merged 17 commits into
mainfrom
feature/api-parity-p1-validate

Conversation

@franciscbalint

@franciscbalint franciscbalint commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Add ZBValidateOptions overloads for validate / validateBatch
  • Parse active_in_days / active_first_seen on ZBValidateResponse

Priority 2 — /v2/scoring (real-time AI scoring)

  • scoreEmail (GET) and scoreEmailPost (form POST) on main API base URL
  • ZBScoreEmailResponse (email, score)

Test plan

  • ctest in build/ (53 tests passed)

Made with Cursor

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

  • Priority3Options.h + Priority3.cpp: email finder / domain search, list evaluator, sendEvaluatedFile, filters, S3 validate/score/export
  • Raw JSON callbacks for filter/list-evaluator GET responses

Test plan

  • ctest — includes Tests.testValidateS3FileValid (53 tests)

Validate options, Inbox Placement Tester, list evaluator base URL

Ported from the Python SDK (407f0a8, a6710e5).

Added

  • ZBValidateOptions gains domainInfo, greylistProcessing, callbackUrl. Booleans go on the wire as "true"/"false" on both the query and the form POST. callbackUrl is rejected without greylistProcessing = true, and when blank, before any request is sent.
  • ZBValidateResponse gains domainWebsiteExists, domainRegistrantCompanyName, and message (the API's Message key, returned while a greylist retry is still running).
  • Inbox Placement Tester on https://bulkapi.zerobounce.net/api/ipt: getInboxPlacementProviders, getInboxPlacementTestTypes, createInboxPlacementTest, getInboxPlacementTest, listInboxPlacementTests, deleteInboxPlacementTest, getInboxPlacementWebhook, setInboxPlacementWebhook. API key on the query string, camelCase JSON bodies, unset fields omitted.
  • setListEvaluatorBaseUrl / setInboxPlacementBaseUrl override those base URLs.
  • BaseRequestHandler gains Put and Delete, used only by the Inbox Placement Tester. Both have default implementations, so any existing subclass keeps compiling.

Changed

  • List evaluator uses its own base URL instead of building the path from the bulk base; the file id is URL-encoded as one path segment. sendEvaluatedFile stays on the bulk base.
  • An error body whose JSON carries error, message, error_message, or detail now reports that text rather than the whole JSON document.

S3 — removed, then restored

  • An earlier push removed validateS3File, scoreS3File, exportS3File, listS3files. That was wrong: they are documented, at https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api. All of them, the ZBS3ListFileEntry type, the option structs and the tests have been restored; only the parity work above remains.
  • AGENTS.md keeps the "only documented endpoints" rule and now points at where the S3 docs live.
    New: the rest of the documented bulk surface

Seven endpoints that are documented but had never been in this SDK:

All six file jobs take a new BulkFileJobOptions and build their form through one bulkFileJobFields helper, which sends has_header_row and remove_duplicates as the strings true/false and omits remove_duplicates and return_url until they are set.

Two shapes differ from their S3 counterparts and are easy to get wrong:

  • validateSFTPFile's file_name is optional, so BulkFileJobOptions::fileName is an optional — leaving it unset uses the input bucket from the account's settings. Every other job requires a name, and a blank one is still rejected.
  • exportSFTPFile takes export_path, not the export_bucket that exportS3File sends. std::nullopt uses 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 and their options structs are untouched.

Tests: 88 pass, up from 77. Offline only — no live API calls. The offline recorder keeps the full URL and raw body so the tests can assert on path encoding and JSON shape.


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.
  • 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 the parameter is now a file id, not a path.
  • The S3 and SFTP jobs require file_name and return_url — validateSFTPFile included, although the docs list file_name as optional for that one.

A bug the live run exposed. {"success": false} on an HTTP 200 reached the success callback: the API answers an unknown bulk file id that way. It now goes to the error callback, and ZBErrorResponse::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 key with surrounding whitespace or control characters is refused before it can be quoted unredacted by the transport. BaseRequestHandler gains a Delete overload that carries a body, because the Warmup DELETE /custom-engagement needs one; it has a default implementation, so existing subclasses keep compiling.

Testing. 120 offline tests pass (ctest), no network. The live suite is its own executable, built only with -DZEROBOUNCE_BUILD_LIVE_TESTS=ON, so a normal build and CI never compile 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 17 commits October 18, 2026 00:59
Support verify_plus, activity_data, and timeouts for validate/validatebatch with client-side range checks.

Co-authored-by: Cursor <cursoragent@cursor.com>
MockRequestHandler now captures lastGetUrl for the two-argument doGet used by sendRequest.

Co-authored-by: Cursor <cursoragent@cursor.com>
Expose real-time AI scoring on the main API with ZBScoreEmailResponse and unit tests aligned with JS #38.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add Priority3Options and bulkapi helpers aligned with Java/Go surface.

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.
Introduce ZBS3ListFileEntry and GET bulkapi /listS3files helper.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
HTML access-denied pages, empty bodies, and account-level batch failures were returned as raw pages or successful results. The offline suite records each request and fails on those cases.

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 domainInfo, greylistProcessing and callbackUrl, sent
  as the strings "true"/"false" on both the query and the form POST.
  callbackUrl without greylistProcessing = true, and a blank one, are rejected
  before any request goes out.
- ZBValidateResponse carries domainWebsiteExists, domainRegistrantCompanyName
  and message, the last read from the API's "Message" key, which a greylist
  pickup returns while the retry is still running.
- Inbox Placement Tester 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. BaseRequestHandler gains Put and Delete for it; both have default
  implementations so existing subclasses keep compiling.
- List evaluator requests use listEvaluatorBaseUrl instead of building the path
  from the bulk base, and the file id is URL-encoded as one path segment.
  sendEvaluatedFile stays on the bulk base. Both new base URLs are settable.
- An error body whose JSON carries error, message, error_message or detail now
  reports that text instead of the raw JSON. "detail" is what the Inbox
  Placement Tester (Django REST framework) returns.

Removes validateS3File, scoreS3File, exportS3File and listS3files along with
ZBS3ListFileEntry, the S3 option structs, tests and changelog entries:
https://www.zerobounce.net/docs/ does not document those endpoints, and
AGENTS.md now records that the SDK covers only documented ones.

The offline recorder keeps the full URL and raw body so the new tests can
assert on path encoding and JSON shape.

Tests: 84 pass (was 77).

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 validateS3File, scoreS3File, exportS3File and listS3files, the
ZBS3ListFileEntry header and source, the S3 option structs, the offline and
legacy tests, and the changelog entries. 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: 86 pass.

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 take the new BulkFileJobOptions and build their form through
bulkFileJobFields, 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:
- validateSFTPFile's file_name is optional, so BulkFileJobOptions::fileName is
  an optional; leaving it unset uses the input bucket from the account's
  settings. Every other job requires a name, and a blank one is still rejected.
- exportSFTPFile takes export_path, not the export_bucket that exportS3File
  sends, and std::nullopt uses the export path from those settings.

The existing S3 methods and their options structs are untouched.

Tests: 88 pass (was 86). Offline only — the fake handler'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 AiAgentMailbox to the enum and to the NLOHMANN_JSON_SERIALIZE_ENUM map,
following RoleBasedCatchAll. Both are required: the macro needs the mapping
to parse the wire value.

Unknown values were already safe: NLOHMANN_JSON_SERIALIZE_ENUM falls back to
the first entry in its map, which is None, so a sub_status added after this
SDK shipped parses to None rather than throwing. A test pins that 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 an Authorization: Bearer
  header, not an api_key parameter, so the key no longer reaches a query
  string that gets logged.
- sendEvaluatedFile takes the list evaluator's file_id as a form field,
  not a file upload. Its parameter 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. ZBErrorResponse 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. 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 key is masked out of every
message, and the SMTP username and password out of errors from that
method.

BaseRequestHandler gains a DELETE-with-body overload, because the
Warmup API's DELETE /custom-engagement carries its arguments in a JSON
body. It has a default implementation, so existing subclasses keep
compiling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
live/live_api.cpp calls every SDK method against the real API. It builds
only with -DZEROBOUNCE_BUILD_LIVE_TESTS=ON, so a normal build and CI
never compile it and ctest never runs 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: 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, 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 48d3f64 into main Oct 24, 2026
6 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