Skip to content

feat: validate WHOIS and greylist options, Inbox Placement Tester, S3 finder/domain search and SFTP bulk - #20

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

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

Conversation

@franciscbalint

@franciscbalint franciscbalint commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Rebuilt on top of main after PR #19 landed. That PR already brought TZbValidateOptions, real-time AI scoring, the filters, the list evaluator and the first four S3 jobs in a ZbPriority3 unit — so this branch now adds only what is still missing, on main's structure, rather than a second copy of that work.

Ports the rest of 407f0a8 and b0444f8 from the Python SDK. Everything is documented at https://www.zerobounce.net/docs/.

Validate options

TZbValidateOptions gains DomainInfo, GreylistProcessing and CallbackUrl. A -1 field is not sent, and booleans go on the wire as the strings true/false.

CallbackUrl is where the greylist retry posts its result, so it is rejected without GreylistProcessing = 1, and when blank — before any request is sent.

TZbValidationResult carries DomainWebsiteExists, DomainRegistrantCompanyName and Message.

The rest of the documented bulk surface

All six share TZbBulkFileJobOptions and P3BulkRootFileJob.

Two shapes differ from the rest and are easy to get wrong:

  • ZbValidateSFTPFile's FileName is optional — leaving it empty uses the input bucket from the account's settings. Every other job requires it.
  • ZbExportSFTPFile sends export_path, not the export_bucket that ZbExportS3File sends.

Inbox Placement Tester

The API key goes on the query string, bodies are camelCase JSON, and unset fields are left out. Spends the account's inbox-test credits.

ZbUtility

ZbListEvaluatorURL and ZbInboxPlacementURL are separate base URLs rather than paths built from BULK_URI. Also new: ZBSendRequest (explicit method plus optional body, which the Tester's PUT and DELETE need), EncodePathSegment — EncodeParam is a query encoder and can turn a space into +, a literal plus inside a path — and ZbLastRequestMethod / ZbLastRequestBody so the offline tests can assert on the body that actually went out.

Changed

  • ZbGetListEvaluatorResult uses ZbListEvaluatorURL, encodes the file id as a single path segment, and rejects a blank id before a request is sent.
  • ZbExportS3File's ExportBucket is now optional, matching the documented endpoint: an empty string uses the export bucket from the account's settings. A whitespace-only value is still rejected. The new export test caught this.

Fixed — two real bugs the new tests caught

  1. A validation response without processed_at no longer raises. The greylist-pending response carries Message and no date, and the parser was raising Invalid date/timestamp : "0000-00-00 00:00:00.000" on it.

  2. TTestOffline.TestEndpointsAndOptions was reaching the live API. It points the local stub at ZbApiBaseURL, BULK_URI and ZbBulkRootURL, but the list evaluator had just moved to its own base URL, which still defaulted to bulkapi.zerobounce.net. UseStub now points the two new base URLs at the stub as well. The symptom was the whole --all run hanging; the real response was {"success":"False","message":["api_key is invalid"]}.

Test plan

  • FPC build + unit_tests_console --all in the Ubuntu 22.04 FPC image — 91 run, 0 errors, 0 failures, up from 77 on main
  • Each suite also run in isolation, and the whole suite run with --network none to prove nothing reaches the live API

🤖 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.


Parity with the Python SDK, verified against the live API

Brings this SDK to the same surface as the (merged) Python SDK and checks every
call against the real API. Where the live API disagrees with the docs, the API
wins and the call site says so in a LIVE: comment.

Corrected against the live API

  • 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"
    — an error that looks nothing like a bad field.
  • The Inbox Placement Tester authenticates with Authorization: Bearer,
    not the api_key query parameter the rest of the API takes. As a parameter
    the request is rejected as unauthorized, and the key would end up in whatever
    logs the query string. Its listing is GET /tests (plural); /test answers
    Method "GET" not allowed.
  • Every S3/SFTP job requires both file_name and return_url,
    validateSFTPfile included, although the docs call its file_name optional.
    Both are now checked before the request leaves.
  • send-evaluated-file takes the file id as a form field, not as a path
    segment.
  • A "success": false field on an HTTP 200 is a failure, not a result.
    getfile and deletefile report an unknown or deleted file id that way, so
    the response now raises, with the HTTP status kept on ZbException.

Added

  • ZbWarmup — a client for the Warmup API,
    a separate product with its own host, its own team-scoped key
    (ZbWarmupInitialize, sent in an API-Key header) and all 26 documented
    endpoints. Two more live findings are baked in: check-domain-dmarc takes an
    email address (a bare domain is answered "Please provide a valid email
    address"
    ), and test-smtp-configuration answers a failed SMTP login with
    HTTP 500. The Warmup key and the SMTP credentials are redacted out of any
    error that escapes the client.
  • Safety rails in ZbUtility — a key with surrounding whitespace or
    control characters is rejected (it would otherwise be quoted unredacted in a
    transport error); only https:// URLs are called (plus loopback, so an
    offline test can use a local stub); a . or .. path id is rejected;
    redirects are never followed; RedactSecrets also strips the url-encoded
    key and runs on every exception built from a response; and a regional base
    URL paired with the single global bulk host warns once.
  • live/live_api.lpr, an opt-in live suite. It is its own program, outside
    tests/, so unit_tests_console never builds or runs it. Keys come from the
    environment only; ZEROBOUNCE_LIVE_SPEND=1 and ZEROBOUNCE_LIVE_CHANGES=1
    opt in to calls that spend credits or make reversible account changes, and
    everything the changes tier creates is removed again in the same run.
    It never creates an Inbox Placement test, sets the Inbox Placement webhook,
    or starts ZB managed Warmup onboarding: the API cannot undo those.
  • ZbPriority3 is now registered in the Delphi package too, where it had been
    missing, alongside the new ZbWarmup.

Docs

README.md and documentation.md had no entry for ZbPriority3 at all; both
now document it and ZbWarmup. AGENTS.md gains the live-API-wins rule, the
safety rails, and a release rule: run the live suite by hand with real keys
before tagging, because the offline suite cannot catch an API that has changed
shape underneath the SDK.

Test plan

  • FPC build + unit_tests_console --all — 112 run, 0 errors, 0 failures
  • Live suite against the real API with both flags set —
    40 passed, 0 failed, 4 skipped (the four skips are the per-address
    Warmup statistics; this team has no sending account)
  • No key is written to a file, a commit, a log or a fixture; keys were
    passed as environment variables only

🤖 Generated with Claude Code

…TP bulk

Rebuilt on top of main after PR #19 landed, which already brought
TZbValidateOptions, real-time AI scoring, the filters, the list evaluator and
the first four S3 jobs in a ZbPriority3 unit. This branch now adds only what is
still missing, on main's structure, rather than a second copy of that work.

Ports the rest of 407f0a8 and b0444f8 from the Python SDK:

- TZbValidateOptions gains DomainInfo, GreylistProcessing and CallbackUrl. A
  -1 field is not sent; booleans go on the wire as the strings true/false.
  CallbackUrl is where the greylist retry posts its result, so it is rejected
  without GreylistProcessing = 1, and when blank, before any request is sent.
- TZbValidationResult carries DomainWebsiteExists,
  DomainRegistrantCompanyName and Message.
- ZbEmailFinderS3File and ZbDomainSearchS3File, from
  https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api
- ZbValidateSFTPFile, ZbScoreSFTPFile, ZbEmailFinderSFTPFile,
  ZbDomainSearchSFTPFile and ZbExportSFTPFile, from
  https://www.zerobounce.net/docs/api-integrations/sftp_bulk_api
  All six share TZbBulkFileJobOptions and P3BulkRootFileJob.
- The Inbox Placement Tester: the API key goes on the query string, bodies are
  camelCase JSON, and unset fields are left out.

ZbUtility gains ZbListEvaluatorURL and ZbInboxPlacementURL, ZBSendRequest
(explicit method plus optional body, which the Tester's PUT and DELETE need),
EncodePathSegment, and ZbLastRequestMethod / ZbLastRequestBody for the tests.

Changed:
- ZbGetListEvaluatorResult uses ZbListEvaluatorURL, encodes the file id as a
  single path segment, and rejects a blank id before a request is sent.
- ZbExportS3File's ExportBucket is optional, matching the documented endpoint:
  an empty string uses the export bucket from the account's settings. A
  whitespace-only value is still rejected. The new export test caught this.

Fixed:
- A validation response without processed_at no longer raises. The
  greylist-pending response carries Message and no date, and the parser was
  raising "Invalid date/timestamp" on it.
- TTestOffline.TestEndpointsAndOptions was reaching the LIVE API. It points the
  local stub at ZbApiBaseURL, BULK_URI and ZbBulkRootURL, but the list
  evaluator had just moved to its own base URL, which still defaulted to
  bulkapi.zerobounce.net. UseStub now points the two new base URLs at the stub
  as well. Symptom was the whole --all run hanging; the real response was
  {"success":"False","message":["api_key is invalid"]}.

Tests: 91 run, 0 errors, 0 failures — up from 77 on main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@franciscbalint
franciscbalint force-pushed the feature/api-parity-p1-validate branch from 64df050 to 9bb371f Compare October 23, 2026 22:12
@franciscbalint franciscbalint changed the title feat: validate options, AI scoring, filters, list evaluator, Inbox Placement Tester and the S3/SFTP bulk jobs feat: validate WHOIS and greylist options, Inbox Placement Tester, S3 finder/domain search and SFTP bulk Oct 23, 2026
franciscbalint and others added 7 commits October 24, 2026 02:31
The API now returns ai_agent_mailbox under both the valid and catch-all
statuses. No new status was added.

No source change is needed here. Unlike the other SDKs, this one has no
sub_status enum or constant list: TZbValidationResult.SubStatus is a plain
String assigned straight from the JSON, so ai_agent_mailbox already reaches
callers, and so does any sub_status added in future.

Adds tests that pin both: ai_agent_mailbox under valid and under catch-all,
and an unrecognised value passing through untouched.

Note for whoever writes these next: ZbValidationFromJson parses processed_at
unconditionally and raises EConvertError when it is absent, so a fixture
cannot be trimmed down to just the fields under test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…jobs against the live API

Verified against the live API; where it disagrees with the docs 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 `Authorization: Bearer`, not
  the `api_key` query parameter the rest of the API takes, and its listing is
  `GET /tests` (plural) — `/test` answers 'Method "GET" not allowed'.
  ZBSendRequestWithHeader carries the one extra header, and ZbLastRequestHeader
  records it for the offline tests.
- Every S3/SFTP job requires both `file_name` and `return_url`, including
  validateSFTPfile, whose file_name the docs call optional.
- send-evaluated-file takes the file id as a form field, not a path segment.
- A `"success": false` field on an HTTP 200 is a failure, not a result: getfile
  and deletefile report an unknown or deleted file id that way, so the response
  now raises with the HTTP status kept on the error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- ZbCheckedApiKey rejects a key with surrounding whitespace or control
  characters, which would otherwise be quoted unredacted in a transport error.
  ZBInitialize runs it; an empty key stays deferred to ZbRequireApiKey.
- ZbRequireHttpsUrl allows https only, plus loopback so an offline test can
  point the SDK at a local stub. Every transport checks the URL it is about to
  call through ZbCheckedUrl.
- ZbPathSegment rejects a '.' or '..' id, which would walk the request out of
  the endpoint it was meant for. The list evaluator file id and the Inbox
  Placement test id go through it.
- Redirects are no longer followed (a redirect could send the key to another
  host) and a 3xx is reported as such.
- RedactSecrets also strips the url-encoded form of the key, and every
  ZbException built from a response runs through it.
- A regional base URL paired with the single global bulk host warns once:
  ZeroBounce documents no regional variant of bulkapi.zerobounce.net.

Also adds ZbJsonEscape and ZBSendForeignKeyRequest, which the Warmup client
needs: a request with its own key, where the validation api_key is neither
required nor sent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Warmup API is a separate product from email validation: its own host, its
own team-scoped key, and an API-Key header instead of an api_key parameter.
ZbWarmupInitialize takes the Warmup key; ZBInitialize stays for the validation
key, and the two never mix.

All 26 documented endpoints, each returning the raw JSON body, or an empty
string for the documented empty successes (HTTP 204 and an empty 200). The two
CSV endpoints return the text and save it as well when given a path.

Arguments are checked before anything is sent: the engagement ranges (standard
rules 0-4, reply_pct 5-45, important_pct 5-40, click_pct 0-70 and at most
important_pct x 1.75), ISO dates, the timezone offset, the email types and the
SMTP port.

Verified against the live API:
- check-domain-dmarc takes an email address. The docs describe a domain, but a
  bare domain is answered with "Please provide a valid email address".
- test-smtp-configuration answers a failed SMTP login with HTTP 500.

The Warmup key and the SMTP credentials are redacted out of any error that
escapes the client. ZbWarmup is registered in all three package files, which
also pick up ZbPriority3 for Delphi, where it had been missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
live/live_api.lpr is its own program, outside tests/, so the offline suite
never builds or runs it. Keys come from the environment only:

  ZEROBOUNCE_API_KEY           the validation API (skipped without it)
  ZEROBOUNCE_WARMUP_API_KEY    the Warmup API (skipped without it)
  ZEROBOUNCE_LIVE_SPEND=1      calls that spend credits
  ZEROBOUNCE_LIVE_CHANGES=1    reversible account changes, undone in the
                               same run even when a check fails

By default it only makes free calls: sandbox addresses, reads, and requests
expected to fail that still prove the API accepts the payload. The free tier
checks that the credit balance did not move.

Never called, because the API cannot undo them: creating an Inbox Placement
test, setting the Inbox Placement webhook, and ZB managed Warmup onboarding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
README.md and documentation.md had no entry for ZbPriority3 at all. Both now
list it and the new ZbWarmup unit, including the shapes that are easy to get
wrong: the bulk root host without /v2, the Bearer key on the Inbox Placement
Tester, file_name and return_url being required on every S3/SFTP job, and
check-domain-dmarc taking an email address.

AGENTS.md gains the live-API-wins rule with the differences found so far, the
safety rails in ZbUtility, and a release rule: run the live suite by hand with
real keys before tagging, because the offline suite cannot catch an API that
has changed shape underneath the SDK.

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 f8f7421 into main Oct 24, 2026
5 of 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