Repository navigation
feat: v2 validate parity (verify_plus, activity_data, batch timeout) - #38
Merged
Merged
Conversation
Expose optional verify_plus, activity_data, and batch timeout per v2 docs; extend validation result types with activity fields. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Add bulk email finder and domain search file jobs, list evaluator, allow/block filters, and S3 validate/score/export helpers aligned with bulkapi and v2 docs. 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.
HTML, empty, and account-level failures now reject with a short message, required column 0 and an explicit false header row are sent, and the default test run checks every public method without the network. Co-authored-by: Cursor <cursoragent@cursor.com>
…st evaluator base URL Ports the Python SDK's parity changes (407f0a8, a6710e5) to this SDK. - validateEmail takes domainInfo, greylistProcessing and callbackUrl, sent as the strings "true"/"false" on both the GET and the form POST. callbackUrl without greylistProcessing: true, and a blank callbackUrl, are rejected before any request goes out. - The validate response type carries domain_website_exists, domain_registrant_company_name and message, the last read from the API's "Message" key while a greylist 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. createRequest gained a jsonBody option for this. - List evaluator requests use their own base URL rather than building the path from the bulk base, and the file id is URL-encoded as one path segment. sendEvaluatedFile stays on the bulk base. init's third argument overrides both this and the Inbox Placement Tester base. - An error body carrying a "detail" string (Django REST framework, used by the Inbox Placement Tester) rejects with that text. Removes listS3files, validateS3file, scoreS3file and exportS3file along with their helper, types, tests and changelog entry: https://www.zerobounce.net/docs/ does not document those endpoints, and AGENTS.md now records that the SDK covers only documented ones. Tests: 122 pass, plus dts-check and the webpack build. 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 listS3files, validateS3file, scoreS3file and exportS3file, the _sendBulkRootFormPost helper, the TypeScript types and declarations, the offline and legacy tests, and the changelog entry. 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, so the next reader does not repeat the same assumption. 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: 125 pass, plus dts-check and the webpack build. 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 share _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 S3 ones and are easy to get wrong: - validateSFTPfile's file_name is optional; omitting it uses the input bucket from the account's settings. Every other job requires it. - exportSFTPfile takes export_path, not export_bucket, and omitting it uses the export path from those settings. Tests: 126 pass, plus dts-check and the webpack build. 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 AI_AGENT_MAILBOX to the ZBValidateSubStatus helper in src/zero-bounce.js and to its type in zeroBounceSDK.d.ts, following ROLE_BASED_CATCH_ALL. sub_status is forwarded as a raw string rather than mapped onto the helper, so an unknown value was already safe here. A test now pins that, alongside valid + ai_agent_mailbox and catch-all + ai_agent_mailbox. 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 on 2026-10-24 and ported from the Python SDK.
- validateBatch sends `timeout` as a JSON string ("30"). As a number the API
rejects the whole batch with the account-level "Invalid API Key or your
account ran out of credits" error. activity_data and verify_plus stay JSON
booleans.
- sendEvaluatedFile takes the list evaluator's file_id as a form field, not a
file upload as the docs describe: an upload is rejected with
"file_id: This field is required". The signature is now
sendEvaluatedFile(fileId); it returns a new validation job's file_id, which
answers "File cannot be found" for a short while after it is accepted.
- The Inbox Placement Tester authenticates with the documented
"Authorization: Bearer <key>" header instead of an api_key query parameter.
Alongside those:
- ZBApiError carries statusCode, so callers can tell a 403 (Cloudflare) from a
400/404 (API) or a 429 (Warmup).
- Error bodies also read error_message, a "detail" validation list
([{loc, msg}] becomes "field: msg"), and a top-level JSON list of strings
(["In-progress tests cannot be deleted."]).
- {"success": false} on an HTTP 200 now raises instead of being returned. Two
existing tests encoded the old behaviour and were updated.
- A 3xx is raised rather than followed.
- Keys are rejected when they carry surrounding whitespace or control
characters, which fetch would otherwise quote unredacted in its own error.
- Network errors are redacted for every form of the key and raised on their
own, so the original (which can hold the key in its URL) is never the cause.
- Request URLs must be https, including every overridable base URL.
- A once-per-client warning when a regional (EU/US) base URL is set but a
request goes to bulkapi.zerobounce.net: there is no regional bulk host.
- The email finder and domain search families get their own overridable base
URLs, like the list evaluator and the Tester already had.
The offline fake now records the Authorization header, so it can answer
requests whose key travels there.
Tests: 129 pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A separate product from email validation, so a separate client rather than methods on ZeroBounceSDK: its own team-scoped key, its own host (https://warmup-api.zerobounce.net, overridable), an API-Key header, and JSON bodies. One method per documented endpoint, 26 in all: - sending accounts: list, get, add (self_managed needs engagement_rule_id 0-4; zb_managed takes email_content and managed_sending), update, delete, changeFromAddress (PUT /smtp-credentials, which stores no credentials), and listFromAddresses; - engagement: rules catalogue, set a standard rule, and get / create / update / delete a custom one; - statistics, seeds and utilities: seeds, seed count, today's progress and sends, emails per day, four statistics endpoints, DMARC check and the SMTP configuration test. The documented ranges are checked before sending: engagement_rule_id 0-4, status_id 0-8, reply_pct 5-45, important_pct 5-40, click_pct 0-70 and at most important_pct x 1.75, timezone_offset -11 to 14, port 1-65535, search_from's character set, and start_date <= end_date. LIVE, verified 2026-10-24: - checkDomainDmarc takes an EMAIL ADDRESS. A bare domain is rejected with "Please provide a valid email address", although the docs describe a domain, so a value without "@" is refused before sending. - testSmtpConfiguration answers any failed SMTP login with HTTP 500. That is an API bug; it is documented on the method. 204 and empty 200 return null. The two downloads return CSV text. Errors carry the HTTP status, read message / error / error_message / detail (including a [{loc, msg}] validation list as "field: msg") and a top-level list of strings, and mask the API key and the SMTP username and password — in network failures too, which are raised on their own so the original is never the cause. The webpack bundle ships only the default export, so Warmup is also hung off ZeroBounceSDK.Warmup to stay reachable from the UMD build. Tests: 143 pass (14 new), plus the .d.ts and TypeScript consumer checks. Note: `npm run dts-check` fails on this machine for a reason unrelated to this change — TypeScript 7.0.2's @typescript/typescript-darwin-arm64 binary is not installed. Installing that package makes it pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verified against the live API on 2026-10-24.
- The six S3 / SFTP file jobs require return_url, which the docs mark optional:
without it the API answers "Provided return URL is not permitted: None".
file_name is required for every job including validateSFTPfile, which the
docs also mark optional ("file_name: This field is required"). Both are
checked before sending, so no request goes out that the API would reject.
remove_duplicates stays the only optional field of the four.
- Filter rule and target are closed sets, checked before sending: rule is
allow or block, target is email, domain, mx or tld. The previous fixtures
used "exclude"/"include", which are not documented values.
Tests: 143 pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tests/live-api.mjs calls every method of both clients against the real API. Jest does not match .mjs, so `npm test` and CI never run it; `npm run test:live` does. It skips without ZEROBOUNCE_API_KEY / ZEROBOUNCE_WARMUP_API_KEY, runs free calls by default and checks the credit balance did not change, and gates the rest behind ZEROBOUNCE_LIVE_SPEND=1 (credit-spending calls and a bulk round trip, deleted again) and ZEROBOUNCE_LIVE_CHANGES=1 (a test filter and a self-managed Warmup sending account, both undone in cleanup even on failure). It never creates an Inbox Placement test, sets the Inbox Placement webhook, or starts ZB-managed Warmup onboarding: the API cannot undo those. No key is written to a file or printed; keys come from the environment and are redacted out of every message the suite prints. getApiUsage now formats a Date argument as the documented YYYY-MM-DD. Passed through unformatted a Date stringifies to "Fri Oct 24 2026 01:00:00 GMT+0400 (Gulf Standard Time)"; in a query string Cloudflare answered that with an HTML "Access denied" page instead of the API answering at all. Strings are untouched. Live run on 2026-10-24 with both flags: 39 passed, 0 failed, 4 skipped (the Warmup statistics endpoints need a registered sending account, and this team has none). It confirmed the four behaviours this branch had taken from the Python SDK's notes rather than from the docs: - validateBatch is accepted with `timeout` as a JSON string; - a complete S3 job payload is accepted, and one without return_url is refused before it is sent; - the Inbox Placement Tester accepts the Authorization: Bearer key; - check-domain-dmarc rejects a bare domain and accepts an email address. Two further live findings, both now reflected in the suite: - activity_data, verify_plus and domain_info are billable add-ons and are charged even for a sandbox address, so the option-heavy validate call sits in the SPEND tier rather than the free one. - A just-accepted bulk job answers "File cannot be found" for a short while, so the round trip retries the delete instead of leaving the job behind. Offline tests: 143 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two CodeQL findings on this PR. warnOnRegionalBulkHost matched "bulkapi.zerobounce.net" as a substring of the whole URL, which also accepts https://evil.example/bulkapi.zerobounce.net and https://bulkapi.zerobounce.net.evil.example. The host is now parsed out and compared on its own, the same way the PHP, C and Pascal SDKs already do it. The regional base URL is matched the same way rather than by substring. The Warmup client trimmed trailing slashes with /\/+$/, which backtracks quadratically on a string of many slashes when the base URL comes from the caller. Replaced with a linear scan. 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.
Summary
activityData/verifyPlusonvalidateEmail(GET/v2/validate) andvalidateBatch(POST/v2/validatebatch).timeout(10–120s) onvalidateBatch.active_in_daysandactive_first_seenon validation result types.Priority 2 — real-time AI scoring
scoreEmail(email, options?)for GET/v2/scoring(default) and POST/v2/scoringwhenoptions.method === "POST"(form-urlencodedapi_key,email).EmailScoringResult,EmailScoringError,ScoreEmailOptions.Priority 3 — bulk finder, list evaluator, filters, S3
sendEmailFinderFile/sendEmailFinderFileStream,getEmailFinderFileStatus,getEmailFinderFile,deleteEmailFinderFile→bulkapi.zerobounce.net/email-finder/*sendDomainSearchFile/sendDomainSearchFileStream, status/get/delete →bulkapi…/domain-search/*sendListEvaluatorFile/sendListEvaluatorFileStream,getListEvaluatorResult,sendEvaluatedFile→bulkapi…/v2/listevaluator/(+/v2/send-evaluated-file/)addFilter,listFilters(GET ormethod: "POST"),deleteFilter→/v2/filters/*on main APIvalidateS3file,scoreS3file,exportS3fileonbulkapiroot (per Postman;remove_duplicates/ optional column fields documented in types)Gaps / notes
listS3filesnot added (out of P3 scope); validate/score field names follow Postman (remove_duplicates).Docs
Test plan
npm run test:unit(94 tests) — includes Priority 3 URL/body mockssdk-docscomposejavascriptimageDeferred
/v2/validatemethod parityMade with Cursor
Validate options, Inbox Placement Tester, list evaluator base URL
Ported from the Python SDK (
407f0a8,a6710e5).Added
validateEmailoptionsdomainInfo,greylistProcessing,callbackUrl. Booleans go on the wire as"true"/"false"on both the GET and the form POST.callbackUrlis rejected withoutgreylistProcessing: true, and a blank one is rejected, before any request is sent.domain_website_exists,domain_registrant_company_name, andmessage(read from the API'sMessagekey, returned while a greylist retry is still running).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.inittakes a third argument to override the list evaluator and Inbox Placement Tester base URLs.Changed
.../v2/listevaluator) instead of building the path from the bulk base; the file id is URL-encoded as one path segment.sendEvaluatedFilestays on the bulk base.Fixed
detailstring (Django REST framework, used by the Inbox Placement Tester) now rejects with that text.S3 — removed, then restored
listS3files,validateS3file,scoreS3file,exportS3file. That was wrong: they are documented, at https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api. All of them, their helper, types and tests have been restored, and only the parity work above remains.AGENTS.mdkeeps the "only documented endpoints" rule and now points at where the S3 docs live.emailFinderS3fileanddomainSearchS3file, and the SFTP variants on the Integrations index, are documented but have never been part of this SDK.Tests: 125 pass, plus
dts-checkand the webpack build. Offline only — no live API calls.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.