Repository navigation
feat: validate WHOIS and greylist options, Inbox Placement Tester, S3 finder/domain search and SFTP bulk - #20
Merged
Conversation
…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
force-pushed
the
feature/api-parity-p1-validate
branch
from
October 23, 2026 22:12
64df050 to
9bb371f
Compare
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>
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.
Rebuilt on top of
mainafter PR #19 landed. That PR already broughtTZbValidateOptions, real-time AI scoring, the filters, the list evaluator and the first four S3 jobs in aZbPriority3unit — 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
407f0a8andb0444f8from the Python SDK. Everything is documented at https://www.zerobounce.net/docs/.Validate options
TZbValidateOptionsgainsDomainInfo,GreylistProcessingandCallbackUrl. A-1field is not sent, and booleans go on the wire as the stringstrue/false.CallbackUrlis where the greylist retry posts its result, so it is rejected withoutGreylistProcessing = 1, and when blank — before any request is sent.TZbValidationResultcarriesDomainWebsiteExists,DomainRegistrantCompanyNameandMessage.The rest of the documented bulk surface
ZbEmailFinderS3File,ZbDomainSearchS3File— https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-apiZbValidateSFTPFile,ZbScoreSFTPFile,ZbEmailFinderSFTPFile,ZbDomainSearchSFTPFile,ZbExportSFTPFile— https://www.zerobounce.net/docs/api-integrations/sftp_bulk_apiAll six share
TZbBulkFileJobOptionsandP3BulkRootFileJob.Two shapes differ from the rest and are easy to get wrong:
ZbValidateSFTPFile'sFileNameis optional — leaving it empty uses the input bucket from the account's settings. Every other job requires it.ZbExportSFTPFilesendsexport_path, not theexport_bucketthatZbExportS3Filesends.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
ZbListEvaluatorURLandZbInboxPlacementURLare separate base URLs rather than paths built fromBULK_URI. Also new:ZBSendRequest(explicit method plus optional body, which the Tester's PUT and DELETE need),EncodePathSegment—EncodeParamis a query encoder and can turn a space into+, a literal plus inside a path — andZbLastRequestMethod/ZbLastRequestBodyso the offline tests can assert on the body that actually went out.Changed
ZbGetListEvaluatorResultusesZbListEvaluatorURL, encodes the file id as a single path segment, and rejects a blank id before a request is sent.ZbExportS3File'sExportBucketis 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
A validation response without
processed_atno longer raises. The greylist-pending response carriesMessageand no date, and the parser was raisingInvalid date/timestamp : "0000-00-00 00:00:00.000"on it.TTestOffline.TestEndpointsAndOptionswas reaching the live API. It points the local stub atZbApiBaseURL,BULK_URIandZbBulkRootURL, but the list evaluator had just moved to its own base URL, which still defaulted tobulkapi.zerobounce.net.UseStubnow points the two new base URLs at the stub as well. The symptom was the whole--allrun hanging; the real response was{"success":"False","message":["api_key is invalid"]}.Test plan
unit_tests_console --allin the Ubuntu 22.04 FPC image — 91 run, 0 errors, 0 failures, up from 77 on main--network noneto prove nothing reaches the live API🤖 Generated with Claude Code
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.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
validatebatchtakestimeoutas a JSON string. Sent as a number theAPI 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.
Authorization: Bearer,not the
api_keyquery parameter the rest of the API takes. As a parameterthe request is rejected as unauthorized, and the key would end up in whatever
logs the query string. Its listing is
GET /tests(plural);/testanswersMethod "GET" not allowed.file_nameandreturn_url,validateSFTPfileincluded, although the docs call itsfile_nameoptional.Both are now checked before the request leaves.
send-evaluated-filetakes the file id as a form field, not as a pathsegment.
"success": falsefield on an HTTP 200 is a failure, not a result.getfileanddeletefilereport an unknown or deleted file id that way, sothe 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 anAPI-Keyheader) and all 26 documentedendpoints. Two more live findings are baked in:
check-domain-dmarctakes anemail address (a bare domain is answered "Please provide a valid email
address"), and
test-smtp-configurationanswers a failed SMTP login withHTTP 500. The Warmup key and the SMTP credentials are redacted out of any
error that escapes the client.
ZbUtility— a key with surrounding whitespace orcontrol characters is rejected (it would otherwise be quoted unredacted in a
transport error); only
https://URLs are called (plus loopback, so anoffline test can use a local stub); a
.or..path id is rejected;redirects are never followed;
RedactSecretsalso strips the url-encodedkey 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, outsidetests/, sounit_tests_consolenever builds or runs it. Keys come from theenvironment only;
ZEROBOUNCE_LIVE_SPEND=1andZEROBOUNCE_LIVE_CHANGES=1opt 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.
ZbPriority3is now registered in the Delphi package too, where it had beenmissing, alongside the new
ZbWarmup.Docs
README.mdanddocumentation.mdhad no entry forZbPriority3at all; bothnow document it and
ZbWarmup.AGENTS.mdgains the live-API-wins rule, thesafety 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
unit_tests_console --all— 112 run, 0 errors, 0 failures40 passed, 0 failed, 4 skipped (the four skips are the per-address
Warmup statistics; this team has no sending account)
passed as environment variables only
🤖 Generated with Claude Code