Skip to content
Open
55 changes: 55 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Agent notes — zero-bounce-java-sdk-setup

## Layout

- `zero-bounce-sdk/` is the published Maven module. `src/` at the repo root is only the sample app.
- `ZeroBounceSDK.java` holds every request. `sendRequest` is the original GET/JSON-POST helper; `sendRequestWithMethod` is the one that can also do form-encoded POSTs and the Inbox Placement Tester's PUT and DELETE.
- `ZBValidateOptions`, `ZBBulkFileJobOptions`: the option objects, each with a package-private `fill` that both builds the request and rejects an invalid combination before anything is sent.
- `ZeroBounceWarmup.java`: a separate client for the Warmup API, a different product with its own host and its own team-scoped key. Not part of `ZeroBounceSDK`.
- `OfflineApiTest.java`, `OfflineWarmupTest.java`: the offline suites. A mocked `HttpURLConnection` answers every call and records the url, method, headers and body, so the assertions are on what actually went on the wire.
- `live/LiveApi.java`: the opt-in live suite. It sits outside the Maven module's source root, so `mvn test` and CI never compile or run it.

## Build and test

Maven runs in Docker (there is no `mvn` on the host, and `mvnw` has no wrapper jar committed):

```sh
cd ../sdk-docs && docker compose run --rm --build java
```

That build runs `mvn -q test` and fails the build if a test fails — but the container's `CMD` then fails on `/root/.m2` permissions, which is not a test failure. To see the test output, or to run against the working tree without rebuilding:

```sh
docker run --rm -u 0 -v "$PWD":/src -w /src/zero-bounce-sdk zerobounce-sdk-java:test mvn -B test
```

The live suite is compiled and run by hand, never by Maven:

```sh
mvn -f zero-bounce-sdk/pom.xml dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt
CP="zero-bounce-sdk/target/classes:$(cat /tmp/cp.txt)"
javac -cp "$CP" -d live/out live/LiveApi.java
export ZEROBOUNCE_API_KEY=... ZEROBOUNCE_WARMUP_API_KEY=...
java -cp "$CP:live/out" com.zerobounce.live.LiveApi # free calls only
ZEROBOUNCE_LIVE_SPEND=1 java -cp "$CP:live/out" com.zerobounce.live.LiveApi
ZEROBOUNCE_LIVE_CHANGES=1 java -cp "$CP:live/out" com.zerobounce.live.LiveApi
```

Keys come from the environment only. Never write one into a file, a commit, a log, a fixture or a message.

## Rules

- **Only endpoints documented at https://www.zerobounce.net/docs/.** Do not add a method for an endpoint the public docs do not describe, even if it exists on the server. Check before concluding something is undocumented: the S3 and SFTP bulk endpoints live under https://www.zerobounce.net/docs/api-integrations, not the main API reference.
- **Four base URLs.** `apiBaseUrl`, `bulkApiBaseUrl` (ends in `/v2`), `bulkApiRootUrl` (the S3/SFTP jobs, **no** `/v2`), and `listEvaluatorBaseUrl` / `inboxPlacementBaseUrl`. The last two are separate hosts rather than paths built from the bulk base, so `initialize(apiKey, apiBaseUrl)` alone leaves them on the live API — use the four-argument `initialize` in tests.
- **Where the live API and the docs disagree, follow the API and say so in a comment at the call site.** The ones found so far: `validatebatch` takes `timeout` as a JSON *string*; the Inbox Placement Tester authenticates with `Authorization: Bearer`, not `api_key`; its listing is `GET /tests`, not `/test`; `send-evaluated-file` takes `file_id` as a form field, not a file upload; the S3 and SFTP jobs require `return_url` and `file_name`, `validateSFTPFile` included; `GET /filters/list` answers with a JSON *array*; the Warmup `/seed-count` answers with a bare number and `check-domain-dmarc` takes an email address, not a domain; and a failed SMTP login in `test-smtp-configuration` comes back as an HTTP 500.
- **`exportSFTPFile` sends `export_path`**, not the `export_bucket` that `exportS3File` sends.
- **`{"success": false}` on an HTTP 200 is a failure.** The API answers an unknown bulk file id that way. `apiSuccessIsFalse` turns it into an error on every request path.
- **A response is not always a JSON object.** Read into a `JsonElement` when it may be an array or a scalar, and remember Gson parses leniently: junk reads as a string instead of raising, so the shape is checked before parsing.
- **`ErrorResponse.parseError` already handles `detail`.** It collects every value of the error JSON, so the Inbox Placement Tester's Django-style `{"detail": ...}` surfaces without a special case. A test pins that.
- **The version lives in the parent pom's `<sdk.version>`.** Both poms read it; bumping one alone breaks the release.
- **Offline tests only, in the Maven module.** Never call the live API from a test under `src/test`. The live suite is the separate, opt-in `live/LiveApi.java`.
- **Errors carry the HTTP status.** `ErrorResponse.getStatusCode()` is set on every path, so callers can tell a Cloudflare 403 from an API 400/404 and a Warmup 429. A failure that never reached the API leaves it 0.

## Releasing

**Run the live suite by hand before every release.** The offline suite proves the SDK sends what we think it sends; only the live suite proves the API accepts it. Run it with real keys, including `ZEROBOUNCE_LIVE_SPEND=1` and `ZEROBOUNCE_LIVE_CHANGES=1`, and do not release on a failure.
198 changes: 198 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,143 @@ Then you can use any of the SDK methods, for example:
});
```

## Validate options

`validate` has an overload that takes `ZBValidateOptions`. An option left unset
is not sent, and booleans go on the wire as the strings `"true"`/`"false"`.

```java
ZBValidateOptions options = new ZBValidateOptions()
.setTimeout(30) // 3-60 seconds
.setActivityData(true) // fills getActiveInDays() / getActiveFirstSeen()
.setVerifyPlus(true)
.setDomainInfo(true) // fills getDomainWebsiteExists() / getDomainRegistrantCompanyName()
.setGreylistProcessing(true)
.setCallbackUrl("https://example.com/greylist")
.setUsePost(true); // form-encoded POST /v2/validate instead of GET

ZeroBounceSDK.getInstance().validate("<EMAIL>", null, options,
response -> System.out.println(response),
error -> System.out.println(error.getErrors()));
```

`setCallbackUrl` is where the greylist retry posts its result, so it is rejected
without `setGreylistProcessing(true)` — before any request is sent. While that
retry is still running the API answers with a `Message` instead of a result,
which lands on `response.getMessage()`.

## Real-time AI scoring

```java
ZeroBounceSDK.getInstance().scoreEmail("<EMAIL>", onSuccess, onError); // GET /v2/scoring
ZeroBounceSDK.getInstance().scoreEmail("<EMAIL>", true, onSuccess, onError); // form-encoded POST
```

## Filters

```java
ZeroBounceSDK.getInstance().addFilter("exclude", "domain", "example.com", onSuccess, onError);
ZeroBounceSDK.getInstance().listFilters(false, onSuccess, onError); // true sends a POST
ZeroBounceSDK.getInstance().deleteFilter("exclude", "domain", "example.com", onSuccess, onError);
```

## S3 and SFTP/FTP bulk

Queue a job on a file that already sits in your Amazon S3 bucket, or on your
SFTP/FTP server. See the [S3](https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api)
and [SFTP](https://www.zerobounce.net/docs/api-integrations/sftp_bulk_api) docs.
These live on the bulkapi host *without* the `/v2` path.

```java
ZeroBounceSDK zb = ZeroBounceSDK.getInstance();

// Amazon S3. The file name is the full path, as returned by listS3Files.
zb.listS3Files(null, null, onSuccess, onError); // optional bucket, search

// file_name and return_url are required on every one of these jobs.
ZBBulkFileJobOptions s3 = new ZBBulkFileJobOptions()
.setFileName("lists/leads.csv")
.setHasHeaderRow(true)
.setReturnUrl("https://example.com/zerobounce-done");

zb.validateS3File(s3, onSuccess, onError);
zb.scoreS3File(s3, onSuccess, onError);
zb.emailFinderS3File(s3, onSuccess, onError);
zb.domainSearchS3File(s3, onSuccess, onError);
zb.exportS3File("<FILE_ID>", "my-export-bucket", onSuccess, onError);

// SFTP/FTP. The server, path and protocol come from your ZeroBounce settings.
// The docs list file_name as optional for validateSFTPFile, but the live API
// requires it there too.
ZBBulkFileJobOptions sftp = new ZBBulkFileJobOptions()
.setFileName("my_file.txt")
.setReturnUrl("https://example.com/zerobounce-done");

zb.validateSFTPFile(sftp, onSuccess, onError);
zb.scoreSFTPFile(sftp, onSuccess, onError);
zb.emailFinderSFTPFile(sftp, onSuccess, onError);
zb.domainSearchSFTPFile(sftp, onSuccess, onError);

// Note the field: this one takes export_path, not export_bucket.
// Pass null to use the export path from your settings.
zb.exportSFTPFile("<FILE_ID>", "/exports", onSuccess, onError);
```

## List evaluator

The list evaluator has its own base URL, not a path built from the bulk one, and
the file id is URL-encoded as a single path segment.

```java
ZeroBounceSDK.getInstance().getListEvaluatorResult("<FILE_ID>", onSuccess, onError);

// Submit a finished evaluation for validation. This takes the evaluator's
// file_id as a form field, not a file upload.
ZeroBounceSDK.getInstance().sendEvaluatedFile("<FILE_ID>", onSuccess, onError);
```

## Inbox Placement Tester

Runs on its own host, authenticates with an `Authorization: Bearer` header, and
sends camelCase JSON bodies. It spends the account's inbox-test credits.

```java
ZeroBounceSDK zb = ZeroBounceSDK.getInstance();

zb.getInboxPlacementProviders(onSuccess, onError);
zb.getInboxPlacementTestTypes(onSuccess, onError);

zb.createInboxPlacementTest("Launch",
new ZeroBounceSDK.CreateInboxPlacementTestOptions()
.setTestType(1)
.setSenderEmail("news@example.com")
.setWebhookUrl("https://example.com/ipt-webhook"),
onSuccess, onError);

// Send your campaign to the seeds you got back, then read the results.
zb.getInboxPlacementTest("<TEST_ID>", onSuccess, onError);

zb.listInboxPlacementTests(
new ZeroBounceSDK.ListInboxPlacementTestsOptions().setStatus("finished").setPage(1),
onSuccess, onError);
zb.setInboxPlacementWebhook("https://example.com/ipt-webhook", onSuccess, onError);
zb.getInboxPlacementWebhook(onSuccess, onError);
zb.deleteInboxPlacementTest("<TEST_ID>", onSuccess, onError);
```

## Per-API base URLs

The list evaluator and the Inbox Placement Tester are separate hosts, so
overriding the main base URL alone leaves them pointing at the live API.

```java
ZeroBounceSDK.getInstance().initialize(
"<API_KEY>",
"https://api.zerobounce.net/v2",
"https://bulkapi.zerobounce.net/v2/listevaluator",
"https://bulkapi.zerobounce.net/api/ipt");
```

## Breaking Changes:
**`guessFormat`** has been deprecated. To continue using your existing code, you must migrate to **`findEmail`** or **`findDomain`** .
The change is not a simple one-to-one replacement, as the functionality has been split:
Expand Down Expand Up @@ -756,6 +893,67 @@ You should then go to the [Nexus Sonatype](https://s01.oss.sonatype.org/), login
gpg --list-secret-keys
```

## Warmup API

The Warmup API is a separate product. It has its own host, its own team-scoped key, and its own client; your validation key will not work with it.

```java
ZeroBounceWarmup warmup = ZeroBounceWarmup.getInstance();
warmup.initialize("<YOUR_WARMUP_API_KEY>");

warmup.listSendingAccounts(onSuccess, onError);
warmup.getProgressToday(onSuccess, onError);
warmup.downloadSeeds(csv -> { /* CSV text, columns seed,provider */ }, onError);
```

Add a from-address you run yourself, with one of the five standard engagement rules (0 Default, 1 Slow Pace, 2 Repair, 3 Boost, 4 Protect):

```java
warmup.addSendingAccount("sender@example.com", "self_managed",
new ZeroBounceWarmup.AddSendingAccountOptions().setEngagementRuleId(0),
onSuccess, onError);
```

A custom engagement rule can be changed only once every seven days; inside that window the API answers 429.

```java
warmup.createCustomEngagement("sender@example.com", 20, 20, 35, onSuccess, onError); // reply, important, click %
```

`checkDomainDmarc` takes an **email address**, not a domain — the live API rejects a bare domain even though the docs describe one:

```java
warmup.checkDomainDmarc("sender@example.com", null, onSuccess, onError);
```

Every documented empty success (204, or an empty 200) arrives as a `null` payload. A payload that is not a JSON object — `/seeds` is an array, `/seed-count` a bare number — comes back wrapped under `data`. Failures carry the HTTP status on `ErrorResponse.getStatusCode()`. Nothing is stored by `testSmtpConfiguration`, and the credentials you pass it are masked out of any error it raises.

## Live API suite

`mvn test` never touches the network: the live suite lives outside the Maven module's source root, so CI does not even compile it. It needs real keys, which come from the environment and are never written anywhere:

```bash
mvn -f zero-bounce-sdk/pom.xml dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt
mvn -f zero-bounce-sdk/pom.xml compile
CP="zero-bounce-sdk/target/classes:$(cat /tmp/cp.txt)"
javac -cp "$CP" -d live/out live/LiveApi.java

export ZEROBOUNCE_API_KEY=... # validation API
export ZEROBOUNCE_WARMUP_API_KEY=... # Warmup API
java -cp "$CP:live/out" com.zerobounce.live.LiveApi
```

By default it only makes free calls: sandbox addresses, reads, and requests that are expected to fail but still prove the API accepts the payload. It checks that the credit balance did not move. Two flags opt in to more:

| Flag | What it adds |
| --- | --- |
| `ZEROBOUNCE_LIVE_SPEND=1` | billable calls (scoring, activity, email finder) and a bulk round trip that deletes its own job |
| `ZEROBOUNCE_LIVE_CHANGES=1` | reversible account changes — a test filter and a self-managed Warmup sending account — each undone in cleanup even when a test fails |

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.

Run it by hand before every release: the offline suite proves the SDK sends what we think it sends, and only the live suite proves the API accepts it.

## Publish

1. Bump `sdk.version` in `zero-bounce-sdk/pom.xml`, commit, tag (`vX.Y.Z`), push tag.
Expand Down
Loading
Loading