Repository navigation
fix: cap HTTP response growth in write_callback - #10
Merged
Merged
Conversation
Align JSON and getfile download limits with Rust SDK caps and stop exiting the process on URL-escape allocation failures in query building. Co-authored-by: Cursor <cursoragent@cursor.com>
Add validate_*_with_options APIs, batch body test hook, and active_in_days/active_first_seen on responses. Co-authored-by: Cursor <cursoragent@cursor.com>
Grow validate query URLs with realloc before appending optional params, and copy activity fields in batch response vectors to avoid heap corruption in tests. Co-authored-by: Cursor <cursoragent@cursor.com>
json-c may insert spaces after colons in serialized batch payloads. Co-authored-by: Cursor <cursoragent@cursor.com>
Add ZBScoreEmailResponse plus score_email and score_email_post with form-encoded POST, matching JS scoreEmail parity. Co-authored-by: Cursor <cursoragent@cursor.com>
Add Priority3 C API surface aligned with C++/JS bulkapi endpoints and Unity mock tests for validateS3file. 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.
Wire ZBValidateOptions.use_post for validate_email_with_options and add list_s3_files Unity tests. Co-authored-by: Cursor <cursoragent@cursor.com>
HTML pages, empty bodies, and account-level batch failures were reaching callers as raw payloads or successes. Record each request in the test hook so the default Unity run checks endpoints without the network. 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 domain_info, greylist_processing and callback_url. Booleans go on the wire as the strings true/false on both the GET and the form POST. 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. - 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. - 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 - The Inbox Placement Tester. The API key goes on the query string, bodies are camelCase JSON, unset fields are left out, and it needs PUT and DELETE, so zb_make_request_with_method_and_body is new. - zero_bounce_set_list_evaluator_base_url and zero_bounce_set_inbox_placement_base_url. Changed: - The list evaluator uses its own base URL instead of building the path from the bulk base, and the file id is encoded as a single path segment. get_list_evaluator_result now rejects a blank file id before a request is sent. send_evaluated_file stays on the bulk base. The six S3 / SFTP file jobs share ZBBulkFileJobOptions and p3_bulk_root_file_job, 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: - validate_sftp_file's file_name is optional; leaving it NULL uses the input bucket from the account's settings. Every other job requires it, and a blank name is still rejected. - export_sftp_file sends export_path, not the export_bucket export_s3_file sends. p3_escape_path_segment is new: zb_url_escape is a query encoder and turns a space into "+", which is a literal plus inside a path. The list evaluator file id and the Inbox Placement test id go through it. parse_error already collects every value of an error body, so the Tester's Django-style {"detail": ...} surfaces without a special case; a test now pins that. The new bulk assertions read OfflineRequest.body rather than has_field, because p3_append_form_field does not call zb_note_http_field — that only covers the multipart paths. AGENTS.md records this. Tests: 70 pass, up from 63. Offline only — a fake perform hook 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. Adds AiAgentMailbox to the enum and the matching strcmp arm in sub_status_from_string, following role_based_catch_all. Unknown values were already safe: sub_status_from_string ends in `return None`, so a sub_status added after this SDK shipped maps to None rather than leaving the field uninitialised. A test pins that alongside the two new ones. 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'. zb_make_request_with_extra_header carries the one extra header, and the offline stub reads the key from it the way the API does. - 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 id of a finished evaluation as a form field, not an upload, so its second argument is now a file id. - 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. ZBErrorResponse gained `status_code` so a caller can tell 429 from 500 without parsing the message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- zb_api_key_is_clean rejects a key with surrounding whitespace or control characters, which would otherwise be quoted unredacted in a transport error. Both initializers run it. - zb_check_request_url refuses anything that is not https, so the api_key is never put on the wire in the clear, and every transport calls it before the mock short-circuit so the offline suite sees the same rule. - Redirects are never followed (CURLOPT_FOLLOWLOCATION 0, and only the https protocol is enabled) and a 3xx is reported as a redirect rather than parsed. - p3_escape_path_segment 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. - zb_redact_api_key strips the key out of any message built from a URL. - A regional base URL paired with the single global bulk host warns once: ZeroBounce documents no regional variant of bulkapi.zerobounce.net. 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. ZeroBounceWarmup takes the Warmup key; ZeroBounce stays for the validation key, and the two never mix. All 26 documented endpoints, each handing the raw JSON body to its callback, or an empty string for the documented empty successes (HTTP 204 and an empty 200). The two CSV endpoints hand over the text and write it to a path when one is given. 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, and the key is checked for whitespace and control characters the same way the validation key is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
live/live_api.c is built only with -DZEROBOUNCE_BUILD_LIVE_TESTS=ON, so
`cmake --build build` and CI never compile it and the default test run never
touches the network. 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>
get_file buffers the whole body in memory before writing it. For a list too large to hold, get_file_streaming and scoring_get_file_streaming write it out as it arrives. They are opt-in rather than the default because a streamed body cannot be inspected as a whole, and getfile answers an unknown or deleted file id with a JSON error rather than a file. The first 4 KB are held back and checked, so an error still reaches the error callback and no partial file is left behind; the offline suite drives the same probe-then-stream path through the test hook. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
README.md and documentation.md gain the Warmup API, the list evaluator (including send_evaluated_file now taking a file id), and the streaming download, and lose the two claims the live API contradicts: that the Inbox Placement Tester takes the key on the query string, and that validate_sftp_file's file_name is optional. AGENTS.md gains the live-API-wins rule with the differences found so far, the safety rails, how to build and run the live suite, and a release rule: run it 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>
p3_append_form_field starts from a NULL buffer with a zero capacity and grows it with realloc, which for a NULL pointer is malloc: the block comes back uninitialized. The code then took strlen of it and strcat'ed onto it without terminating it first, so whatever bytes the allocator happened to hand over were sent in front of the first field. It usually looked fine, because a fresh page is zeroed; once the process had allocated and freed enough, it was not. The live suite caught it as a Cloudflare block on add_filter, which also affected send_evaluated_file and the S3/SFTP file jobs. The offline test pins that each of those bodies starts at api_key=. 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. The API sends sub_status_antispam_system. This SDK asked json-c for sub_status_anti_spam_system, with an underscore between anti and spam, so the counter silently stayed 0 on every call. The struct field keeps its name, so callers are unaffected; only the key it reads is corrected. sub_status_leading_period_removed and sub_status_alias_address are returned by the API and were not read at all. Both are now parsed, printed and compared. 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
Priority 2 —
/v2/scoring(real-time AI scoring)score_email(GET) andscore_email_post(form POST) on main API base URLZBScoreEmailResponse(email,score)Priority 3 — bulk finder, list evaluator, filters, S3
ZBPriority3Options.h+Priority3.c: email finder / domain search, list evaluator,send_evaluated_file, filters, S3 validate/score/exportTest plan
./build/bin/ZeroBounceTests(39 tests passed, incl.test_validate_s3_file_valid)Made with Cursor
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.A real bug the live run found
p3_append_form_fieldbuilds every form-encoded body. It starts from aNULLbuffer with a zero capacity and grows it with
realloc, which for aNULLpointer is
malloc: the block comes back uninitialized. The code then tookstrlenof it andstrcat'ed onto it without terminating it first, sowhatever bytes the allocator happened to hand over were sent in front of the
first field.
It usually looked fine, because a fresh page is zeroed. Once the process had
allocated and freed enough, it was not — the live suite hit it as a Cloudflare
block (error 1020) on
add_filter, and it affectedsend_evaluated_fileandthe S3/SFTP file jobs just as much. Fixed, with an offline test pinning that
each of those bodies starts at
api_key=.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,validate_sftp_fileincluded, although the docs call itsfile_nameoptional. Both are now checked before the request leaves.
send_evaluated_filetakes a file id as a form field, not a path toupload. Its second argument changed accordingly — the one breaking change
here.
"success": falsefield on an HTTP 200 is a failure, not a result.get_fileanddelete_filereport an unknown or deleted file id that way,so it now reaches the error callback.
ZBErrorResponse.status_codecarriesthe HTTP status so a caller can tell 429 from 500 without parsing the text.
Added
ZeroBounceWarmup— a client for the Warmup API,a separate product with its own host, its own team-scoped key
(
zero_bounce_warmup_initialize, sent in anAPI-Keyheader) and all 26documented endpoints. Two more live findings are baked in:
check-domain-dmarctakes an email address (a bare domain is answered"Please provide a valid email address"), and
test-smtp-configurationanswers a failed SMTP login with HTTP 500. The Warmup key and the SMTP
credentials are redacted out of any error that escapes the client.
rejected (it would otherwise be quoted unredacted in a transport error); only
https://URLs are called; a.or..path id is rejected; redirects arenever followed and only the https protocol is enabled;
zb_redact_api_keystrips the key out of any message built from a URL; and a regional base URL
paired with the single global bulk host warns once.
get_file_streaming/scoring_get_file_streamingwrite a result filestraight to disk as it arrives rather than buffering it in memory. Opt-in,
because a streamed body cannot be inspected as a whole: the first 4 KB are
held back and checked, so a JSON or HTML error still reaches the error
callback and no partial file is left behind.
live/live_api.c, an opt-in live suite built only with-DZEROBOUNCE_BUILD_LIVE_TESTS=ON, so the default build and CI never compileit. Keys come from the environment only;
ZEROBOUNCE_LIVE_SPEND=1andZEROBOUNCE_LIVE_CHANGES=1opt in to calls that spend credits or makereversible 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.
Docs
README.mdanddocumentation.mdgain the Warmup API, the list evaluator andthe streaming download, and lose the two claims the live API contradicts.
AGENTS.mdgains the live-API-wins rule, the safety 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
cmake --build build+./build/bin/ZeroBounceTests— 92 tests, 0 failures40 passed, 0 failed, 4 skipped (the four skips are the per-address
Warmup statistics; this team has no sending account)
policy blocks freshly built native binaries
passed as environment variables only
🤖 Generated with Claude Code