Repository navigation
feat: v2 validate parity (verify_plus, activity_data, batch timeout) - #43
Merged
Merged
Conversation
Add ZBValidateOptions overloads and activity fields on validation responses. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
HTML access-denied pages and empty bodies were reaching callers, and an account-level ValidateBatch error was treated as success. The offline suite covers the public methods with a fake HttpClient and separate clients per key. Co-authored-by: Cursor <cursoragent@cursor.com>
The changelog now covers the unreleased request limits and the user-facing releases back to 1.3.4, and the agent notes name both version fields. 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. - 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 - The Inbox Placement Tester on https://bulkapi.zerobounce.net/api/ipt, in its own partial class. The API key goes on the query string, bodies are camelCase JSON, unset fields are left out, and it needs PUT and DELETE, so it uses HttpClient.SendAsync directly rather than the existing helpers. - SetListEvaluatorBaseUrl and SetInboxPlacementBaseUrl. Changed: - The list evaluator uses its own base URL 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. - ExportS3File's exportBucket is now optional, matching the documented endpoint: null uses the export bucket from the account's settings. A blank string is still rejected. Fixed: - An error body whose JSON carries a "detail" string now surfaces that text. The Inbox Placement Tester is a Django REST framework app and reports its errors that way, unlike the "error"/"message" keys the rest of the API uses. The six S3 / SFTP file jobs share BulkFileJobOptions 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. The two percent-encoding tests first failed against RecordedRequest.RawUri, which comes from Uri.ToString() and un-escapes %20 back to a space for display. The recorder now also keeps Uri.AbsoluteUri as EscapedUri, which is what goes on the wire; the %2F was correct all along. Tests: 59 pass, up from 50. Offline only — a fake HttpMessageHandler records every request 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. ZBValidateSubStatus is an enum whose wire values live in an explicit converter, so the value is added in three places: the enum member, the ReadJson switch, and the WriteJson switch so it round-trips. Unknown values were already safe: ReadJson's default branch returns 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. NOT YET VERIFIED: the test suite could not be run. Docker cannot reach mcr.microsoft.com to pull dotnet/sdk:8.0 (connection refused) although the host can, and dotnet is not installed locally. These tests need running before this branch is merged. 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. 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 path or
stream.
- 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. ZBApiError and ZeroBounce.LastApiError
carry the HTTP status alongside the short message the failure callback
is given, 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: 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 JToken it is: /seeds answers with an array and /seed-count with a bare number. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ZeroBounceLiveApi calls every SDK method against the real API. It is not in ZeroBounceCSharpSDKSetup.sln, so `dotnet build` and `dotnet test` at the repo root never build or run it; `dotnet run --project ZeroBounceLiveApi` with keys in the environment does. 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>
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.
Priority 2 — real-time AI scoring
ScoreEmail(GET) andScoreEmailPost(form POST) on/v2/scoring.ZBScoreEmailResponse.Validate WHOIS and greylist options, Inbox Placement Tester, SFTP bulk
Ports
407f0a8andb0444f8from the Python SDK, on top of the work above. Everything is documented at https://www.zerobounce.net/docs/.Added
ZBValidateOptionsgainsDomainInfo,GreylistProcessingandCallbackUrl. Booleans go on the wire as"true"/"false"on both the GET and the form POST.CallbackUrlis where the greylist retry posts its result, so it is rejected withoutGreylistProcessing, and when blank, before any request is sent.ZBValidateResponsereadsdomain_website_exists,domain_registrant_company_nameandMessage.Messageis the API'sMessagekey, returned instead of a result while a greylist retry is still running.EmailFinderS3File,DomainSearchS3File— https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-apiValidateSFTPFile,ScoreSFTPFile,EmailFinderSFTPFile,DomainSearchSFTPFile,ExportSFTPFile— https://www.zerobounce.net/docs/api-integrations/sftp_bulk_apiHttpClient.SendAsyncdirectly rather than the existing helpers.SetListEvaluatorBaseUrlandSetInboxPlacementBaseUrl.Changed
SendEvaluatedFilestays on the bulk base.ExportS3File'sexportBucketis now optional, matching the documented endpoint:nulluses the export bucket from the account's settings. A blank string is still rejected.Fixed
detailstring now surfaces that text. The Inbox Placement Tester is a Django REST framework app and reports its errors that way, unlike theerror/messagekeys the rest of the API uses.The two bulk shapes that are easy to get wrong
ValidateSFTPFile'sfile_nameis optional — leavingFileNamenull uses the input bucket from the account's settings. Every other job requires it, and a blank name is still rejected.ExportSFTPFilesendsexport_path, not theexport_bucketthatExportS3Filesends.The SFTP server, path and protocol are never sent by the SDK; they come from the account's ZeroBounce settings.
Note on the encoding tests
Both percent-encoding assertions first failed against
RecordedRequest.RawUri, which comes fromUri.ToString()and un-escapes%20back to a space for display. The recorder now also keepsUri.AbsoluteUriasEscapedUri, which is what goes on the wire; the%2Fwas correct all along.Test plan
dotnet testin the .NET 8 SDK image — 59 pass, up from 50 (9 new inOfflineApiTests)dotnet buildonZeroBounceSDK.csproj— 0 warnings, 0 errorsHttpMessageHandlerrecords every request and answers without the network🤖 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.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:
validatebatchtakestimeoutas 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.Authorization: Bearer, not anapi_keyparameter — the key no longer reaches a query string that gets logged — and its listing isGET /tests.GET /testanswersMethod "GET" not allowed, soListInboxPlacementTestshad never worked.SendEvaluatedFiletakes the list evaluator'sfile_idas a form field. A file upload is rejected withfile_id: This field is required, so it now takes a file id rather than a path or a stream.file_nameandreturn_url—ValidateSFTPFileincluded, although the docs listfile_nameas 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 failure callback.New: the Warmup API client.
ZeroBounceWarmupcovers all 26 documented endpoints of what is a separate product — its own host, its own team-scoped key in anAPI-Keyheader, 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 asnull,CheckDomainDmarcrequires 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 theJTokenit is, because/seedsanswers with an array and/seed-countwith a bare number.Status codes without changing the callbacks. The failure callbacks still take a
string.ZBApiError, reachable asLastApiErroron both clients, carries that same message with the HTTP status, so a caller can tell a Cloudflare 403 from an API 400/404 and a Warmup 429. It is set synchronously, just before the callback runs.Testing. 94 offline tests pass (
dotnet test), no network. The live suite is theZeroBounceLiveApiproject, which is not in the solution, sodotnet buildanddotnet testnever build or run it; the last full run withZEROBOUNCE_LIVE_SPEND=1 ZEROBOUNCE_LIVE_CHANGES=1was 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