Skip to content

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

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

franciscbalint merged 7 commits into
developfrom
feature/api-parity-p1-validate

Conversation

@franciscbalint

@franciscbalint franciscbalint commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Rebuilt on top of develop after PR #27 landed. That PR already brought the validate options, real-time AI scoring, the filters, the list evaluator and the first four S3 jobs — so this branch now adds only what is still missing, on develop'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

validate accepts domain_info, greylist_processing and callback_url. An option left out is not sent, and booleans go on the wire as the strings "true"/"false".

callback_url is where the greylist retry posts its result, so it is rejected without greylist_processing, and when blank — before any request is sent.

The rest of the documented bulk surface

All six share bulk_root_file_job and go out through develop's existing bulk_root_form_post on the bulkapi root.

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

  • validate_sftp_file's file name is optional — passing nil uses the input bucket from the account's settings. Every other job requires it.
  • export_sftp_file sends export_path, not the export_bucket that export_s3_file sends.

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

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.

Base URLs and plumbing

Configuration gains bulk_root_url, list_evaluator_root_url and inbox_placement_root_url. They are separate base URLs rather than paths built from bulk_api_root_url, so overriding that alone would leave a spec pointing at the live API. get_list_evaluator_result now uses the list evaluator's own base URL, encodes the file id as a single path segment, and rejects a blank id before a request.

BaseRequest gains __execute_with_method__ plus the public root_request and ipt_request: _get and _post could not express a form-encoded POST on an arbitrary base URL, or the Tester's PUT and DELETE. They live on BaseRequest so MockRequest inherits them too.

__parse_json_response__ now also raises on a detail key — the Tester is a Django REST framework app and reports its errors that way.

Test plan

  • bundle exec rspec in the Ruby 3.2 image — 134 examples, 0 failures, up from 111 on develop
  • Offline only: every request is stubbed with WebMock, and the argument-validation specs assert that no request was made at all

🤖 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 list_inbox_placement_tests had never worked.
  • send_evaluated_file takes the list evaluator's file_id as a form field. A file upload is rejected with file_id: This field is required, so its argument is now a file id, not a path.
  • The S3 and SFTP jobs require file_name and return_url — validate_sftp_file included, although the docs list file_name as optional for that one.

Two bugs the live run exposed in this SDK.

  • Every filter write raised on success. /filters/add answers with a bare {"Message": "Filter successfully added"}, and a body that is nothing but Message is how the validate endpoints report a rejected request — so add_filter, delete_filter and list_filters turned a success into an error. The rule now skips the three filters paths, and two specs pin both halves of it.
  • {"success": false} on an HTTP 200 was returned as data. The API answers an unknown bulk file id that way, and two specs had pinned that wrong behaviour. It now raises.

New: the Warmup API client. Zerobounce::Warmup 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 nil, check_domain_dmarc requires an email address (the live API rejects a bare domain), and the SMTP test masks the credentials out of every error it raises. Failures are Zerobounce::ApiResponseError, whose #status_code carries the HTTP status.

Testing. 173 offline specs pass (rspec), no network. The live suite (live/live_api.rb) sits outside spec/, so rspec and CI never load 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

…TP bulk

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

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

- validate accepts domain_info, greylist_processing and callback_url. An
  option left out is not sent; booleans go on the wire as the strings
  true/false. callback_url is where the greylist retry posts its result, so it
  is rejected without greylist_processing, and when blank, before any request
  is sent.
- email_finder_s3_file and domain_search_s3_file, from
  https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api
- validate_sftp_file, score_sftp_file, email_finder_sftp_file,
  domain_search_sftp_file and export_sftp_file, from
  https://www.zerobounce.net/docs/api-integrations/sftp_bulk_api
  All six share bulk_root_file_job and go out through develop's
  bulk_root_form_post on the bulkapi root.
- The Inbox Placement Tester: the API key goes on the query string, bodies are
  camelCase JSON, and unset fields are left out.

Configuration gains bulk_root_url, list_evaluator_root_url and
inbox_placement_root_url. They are separate base URLs rather than paths built
from bulk_api_root_url, so overriding that alone would leave a spec pointing at
the live API. get_list_evaluator_result now uses the first of those, encodes
the file id as a single path segment, and rejects a blank id before a request.

BaseRequest gains __execute_with_method__ plus the public root_request and
ipt_request: _get and _post could not express a form-encoded POST on an
arbitrary base URL, or the Tester's PUT and DELETE. They live on BaseRequest so
MockRequest inherits them too.

__parse_json_response__ now also raises on a "detail" key. The Tester is a
Django REST framework app and reports its errors that way.

Tests: 134 examples pass, up from 111 on develop. Offline only — every request
is stubbed with WebMock, and the argument-validation specs assert that no
request was made at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@franciscbalint
franciscbalint force-pushed the feature/api-parity-p1-validate branch from 527ef10 to 191b880 Compare October 23, 2026 21:34
@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 6 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.

Adds AI_AGENT_MAILBOX to Zerobounce::ValidateSubStatus following
ROLE_BASED_CATCH_ALL.

sub_status is read straight off the parsed JSON rather than mapped onto these
constants, so an unknown value already reached the caller untouched -- the
module's own comment says as much. A spec pins that alongside the two new
ones. These three stub the request directly instead of using a cassette,
since they are about a sub_status value rather than any recorded interaction.

No sub_status_ai_agent_mailbox usage counter: it is not documented.

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.

- validate_batch 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.
- send_evaluated_file takes the list evaluator's file_id as a form
  field, not a file upload. Its argument changes accordingly.
- The S3 and SFTP jobs require file_name and return_url,
  validate_sftp_file 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 specs had pinned the wrong
behaviour.

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. The
Zerobounce module stays the validation client; Zerobounce::Warmup 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.

Zerobounce::ApiResponseError carries the HTTP status through
#status_code, so callers can tell a 403 from Cloudflare apart from a
400/404 from the API and a 429 from the Warmup rate limit.
ResponseError.message_from_parts now also reads "detail" lists and
top-level string arrays, which is how the Warmup API reports errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
live/live_api.rb calls every SDK method against the real API. It lives
outside spec/, so `rspec` and CI never load it; running it takes
`ruby -Ilib live/live_api.rb` with 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.

The bug: add_filter raised on success. /filters/add answers with a bare
{"Message": "Filter successfully added"}, and a body that is nothing but
"Message" is how the validate endpoints report a *rejected* request, so
the SDK was turning every filter write into an error. The rule now skips
the three filters paths, and two specs pin both halves of it.

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>
This branch added `bundle exec rubocop` to CI, but the repository has no
RuboCop config and had never been linted, so the step ran every default cop
over the whole codebase and reported 2053 offences. The lint gate was
therefore red from the moment it was added.

Adds a .rubocop.yml pinning TargetRubyVersion to the 2.5 the gemspec promises
and loading rubocop-performance, with the existing offences baselined in a
generated .rubocop_todo.yml. New code has to be clean; the backlog can be
worked down over time.

Generating that baseline surfaced a real bug. response_error.rb built a
validation-error field name with `loc[1..]`, an endless range introduced in
Ruby 2.6. The gemspec declares `required_ruby_version >= 2.5`, where that is
a syntax error, so on 2.5 the gem would fail to load rather than misbehave
subtly. Changed to `loc[1..-1]`, which is equivalent and parses on 2.5.

RuboCop: no offences. RSpec: 173 examples, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@franciscbalint
franciscbalint merged commit b9cf2a8 into develop 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