Repository navigation
feat: v2 validate parity (verify_plus, activity_data, batch timeout) - #44
Merged
Merged
Conversation
Add ZBValidateOptions for validate/validateBatch and activity fields on 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.
Assert the bulk listS3files URL and parsed array response using the existing mock request harness. Co-authored-by: Cursor <cursoragent@cursor.com>
HTML, empty, and account-level batch errors become short exceptions, column 0 and false flags are sent, and redirects are disabled. Offline tests, the changelog, and agent notes record that behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
The notes claimed sendFile sent remove_duplicate and that optional column 0 was omitted. The SDK sends column 0 and has no remove_duplicate argument on sendFile. Co-authored-by: Cursor <cursoragent@cursor.com>
…st evaluator base URL Ports the Python SDK's parity changes (407f0a8, a6710e5) to this SDK. - ZBValidateOptions 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. - ZBValidateResponse carries domainWebsiteExists, domainRegistrantCompanyName and message. ZBResponse already camel-cases the API's "Message" key onto that last property, which a greylist pickup returns while the retry runs. - Inbox Placement Tester as its own trait 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 rather than in the body, bodies are camelCase JSON, and only fields the caller set are sent. - 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. initialize's third and fourth arguments override both base URLs. - An error body carrying a "detail" string (Django REST framework, used by the Inbox Placement Tester) raises with that text. Removes validateS3File, scoreS3File, exportS3File and listS3Files along with their helpers, the now-unused BulkApiRootUrl constant, tests and changelog entries: https://www.zerobounce.net/docs/ does not document those endpoints, and AGENTS.md now records that the SDK covers only documented ones. Tests: 62 pass (1 pre-existing skip). 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 validateS3File, scoreS3File, exportS3File and listS3Files, the buildValidateS3Fields, postBulkRootForm and appendOptionalBool helpers, the BulkApiRootUrl constant, the offline and legacy tests, and the changelog entries. 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. 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: 64 pass, 1 pre-existing skip. 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 the export_bucket that exportS3File uses, and omitting it uses the export path from those settings. Tests: 66 pass, 1 pre-existing skip. 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 ZBValidateSubStatus::AiAgentMailbox following RoleBasedCatchAll, so
callers have a named constant to compare against.
On unknown values: ZBValidateResponse::getValue() branches on
$classKey === "sub_status", but ZBResponse::deserialize() camel-cases the key
to "subStatus" before calling it, so that branch never runs and the raw value
is assigned unchanged. Deliberately left alone: pass-through is the safer
outcome, because activating the branch would blank out any sub_status the
installed SDK predates -- including ai_agent_mailbox for anyone on an older
version. A test pins the current behaviour so it cannot regress silently.
("status" has no underscore, so its own normalisation does run.)
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. - 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. - send-evaluated-file takes the list evaluator's file_id as a form field, not a file upload. - The S3 and SFTP bulk jobs require return_url and file_name; without them the API answers "This field is required". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The http:// stream wrapper reports its status only through PHP's predefined response-header variable, which the engine makes visible to the function that called file_get_contents and to no one else. The SDK read it from a helper, where it is always empty, so every GET was recorded as HTTP 0 and an error status never reached readJsonBody: a 404 from the list evaluator came back as a successful result. Every verb now goes through curl, which reports the status directly. That also removes the last read of the variable PHP 8.5 deprecates, so the legacy shim and its two helpers are gone and the guard test can assert the stronger property across the whole of src/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tatus The live API answers an unknown bulk file id with an HTTP 200 whose body says the call failed. Returning that as a result let callers read an error payload as data. ZBApiException now carries the HTTP status through getStatusCode(), so callers can tell a 403 from Cloudflare apart from a 400/404 from the API and a 429 from the Warmup rate limit. request() rebuilds rather than rethrows so the message stays redacted and the status survives. 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. Verified live. Two 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 SMTP username and password are masked out of every error from that method. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Calls every SDK method against the real API. phpunit.xml only includes test/, so `composer test` and CI never run it; `composer run test:live` does, and only 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: 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>
Parity with the Python SDK: - getCredits, getApiUsage, getActivity, scoreEmail, findEmail and findEmailFormat take $usePost. The API accepts the same parameters form-encoded, which keeps the key out of request logs. - getApiUsage formats its dates. A DateTime cast to a string became the whole locale-formatted stamp, which the API rejects and Cloudflare can block outright; a YYYY-MM-DD string is accepted too, anything else is refused before sending. - An API key with surrounding whitespace or control characters is rejected, because the transport would otherwise quote it unredacted in its own error. An empty key stays deferred, so the existing message naming initialize() is unchanged. - Every base URL must be https, and so must every URL a request is actually sent to. - A file or test id of "." or ".." is refused. Everything else, slashes included, is still percent-encoded into one path segment. - A regional base URL paired with the single global bulk host warns once per client: ZeroBounce documents no regional host for bulk files, the list evaluator, S3/SFTP or the Inbox Placement Tester. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
getListEvaluatorResult returns ZBListEvaluatorResponse: the documented fields are named, and everything the API sent is still on ->data, so a field this version does not name yet is not lost. ZBGetFileOptions gains $stream. With it, a result file is written to disk in chunks rather than held in memory: the bytes land in a temporary file beside the download path and are moved into place only when the transfer completes, so a failed download leaves nothing behind. The first 64 KB are kept in memory so an error body is still recognised and reported as a short message rather than saved as a result file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
README gains a Warmup API section, the opt-in live suite and its two flags, the streaming download option, and the $usePost argument. 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]. composer: `lint` covers live/, the phpunit floor is the current patch release, and composer.lock is refreshed. `composer audit` reports no advisories. 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. ZBResponse::Deserialize converts a snake_case key to camelCase and skips any key with no matching property. Four counters convert to names that are not the ones declared: sub_status_antispam_system -> subStatusAntispamSystem sub_status_greylisted -> subStatusGreylisted sub_status_unroutable_ip_address -> subStatusUnroutableIpAddress sub_status_mail_server_did_not_respond -> subStatusMailServerDidNotRespond against properties spelled $subStatusAntiSpamSystem, $subStatusGreyListed, $subStatusUnRoutableIpAddress and $subStatusMailServerDidNotResponse. All four were therefore skipped and stayed null on every call. Renaming the properties would break callers, so ZBApiUsageResponse overrides getClassKey for those four instead. sub_status_leading_period_removed and sub_status_alias_address are returned by the API and had no property at all; both are now declared and read. 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.Priority 3 — bulk S3 / finder / filters
listS3Files, validate/score S3, email finder, filters, list evaluator (see branch diff).Polish (latest)
testListS3FilesCallsBulkListS3files(mocklastRequestUrl+ JSON array response).Test plan
testScoreEmailin PHPUnit suitetestListS3FilesCallsBulkListS3filesValidate options, Inbox Placement Tester, list evaluator base URL
Ported from the Python SDK (
407f0a8,a6710e5).Added
ZBValidateOptionsgainsdomainInfo,greylistProcessing,callbackUrl. Booleans go on the wire as"true"/"false"on both the GET and the form POST.callbackUrlis rejected withoutgreylistProcessing = true, and when blank, before any request is sent.ZBValidateResponsegainsdomainWebsiteExists,domainRegistrantCompanyName, andmessage(the API'sMessagekey, returned while a greylist retry is still running).src/ZeroBounceInboxPlacement.php) onhttps://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.initializetakes optional third and fourth arguments 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 raises with that text.S3 — removed, then restored
validateS3File,scoreS3File,exportS3File,listS3Files. That was wrong: they are documented, at https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api. All of them, their helpers, theBulkApiRootUrlconstant and the tests have been restored; only the parity work above remains.AGENTS.mdkeeps the "only documented endpoints" rule and now points at where the S3 docs live.New: the rest of the documented bulk surface
Seven endpoints that are documented but had never been in this SDK:
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_apiAll six file jobs go through one
bulkRootFileJobhelper, which sendshas_header_rowandremove_duplicatesas the stringstrue/falseand omitsremove_duplicatesandreturn_urluntil they are set.Two shapes differ from their S3 counterparts and are easy to get wrong:
validateSFTPFile'sfile_nameis optional — omitting it uses the input bucket from the account's settings. Every other job requires it.exportSFTPFiletakesexport_path, not theexport_bucketthatexportS3Fileuses. Omitting it uses the export path from those settings.The SFTP server, path and protocol are never sent by the SDK; they come from the account's ZeroBounce settings.
Tests: 66 pass, 1 pre-existing skip. 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.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 rather than by reading the docs:
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, and its listing isGET /tests.GET /testanswersMethod "GET" not allowed.send-evaluated-filetakes the list evaluator'sfile_idas a form field. A file upload is rejected withfile_id: This field is required.return_urlandfile_name.check-domain-dmarctakes an email address, not the domain the docs describe, and a failed SMTP login intest-smtp-configurationcomes back as an HTTP 500.Two bugs the live run exposed in this SDK.
GETreported HTTP 0. Thehttp://stream wrapper reports its status only through PHP's predefined response-header variable, which the engine makes visible to the function that calledfile_get_contentsand to no one else. The SDK read it from a helper, so it was always empty — an error status never reached the response check, and a 404 from the list evaluator came back as a successful result. Every verb now goes through cURL, which also removes the last read of the variable PHP 8.5 deprecates, so the legacy shim is gone and the guard test can assert the stronger property across all ofsrc/.{"success": false}on an HTTP 200 was returned as data. The API answers an unknown bulk file id that way. It now raises, andZBApiException::getStatusCode()carries the HTTP status so callers can tell a Cloudflare 403 from an API 400/404.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, and the SMTP test masks the credentials out of every error it raises.Also added
$usePostongetCredits,getApiUsage,getActivity,scoreEmail,findEmailandfindEmailFormat: the same parameters form-encoded, which keeps the key out of request logs.getApiUsageformats its dates. ADateTimewas interpolated whole, so the request carried a locale-formatted timestamp that the API rejects and Cloudflare can block outright.ZBGetFileOptions::$streamfor large result files: written in chunks to a temporary file beside the download path, moved into place only when complete, with the first 64 KB kept so an error body is still recognised rather than saved as a result.getListEvaluatorResultreturns a typedZBListEvaluatorResponse; everything the API sent is still on->data..and..refused as path ids, and a once-per-client warning when a regional base URL is paired with the single global bulk host.Testing. 112 offline tests, 1046 assertions, no skips, no network. The live suite (
composer run test:live) is excluded fromcomposer testand from CI and needs keys from the environment; the last full run withZEROBOUNCE_LIVE_SPEND=1 ZEROBOUNCE_LIVE_CHANGES=1was 41 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 test fails.composer auditreports no advisories.No version bump, tag or publish.
🤖 Generated with Claude Code