Java SDK
Connect your Java service to customer data, events, and prepared campaigns in maxclicks. Use resource methods to call the Public API and typed exceptions to handle failures.
The Java package is ai.maxclicks. The current version is 1.1.0.
The Maven Central release is pending. Build the source artifact into your local repository as shown below, or follow the HTTP quickstart.
Current API coverage
This guide documents the existing source client interface. The current REST API also exposes event readiness, event identity changes, record deletion status, and workflow history cursors that this client does not yet wrap. Event receipts and deletion results have changed; older static models may omit fields or expect the former { id, deleted } shape.
Use direct HTTP for those operations and verify the current coverage table before adopting a wrapper. Auto-pagination still stops at the API's traversal cap; an iterator or method named all does not guarantee a full workspace export.
Install
Maven Central carries nothing under ai.maxclicks yet. Until it does, publish the artifact to your local Maven repository from source and resolve it from there.
git clone https://github.com/maxclicks-ai/maxclicks-java.git
cd maxclicks-java
gradle publishToMavenLocal
Client
Build a client with Maxclicks.builder(). Pass apiKey, or set the MAXCLICKS_API_KEY environment variable and call .build() with no key.
import ai.maxclicks.Maxclicks;
import ai.maxclicks.models.Me;
import ai.maxclicks.models.MaxclicksRecord;
import java.util.Map;
Maxclicks mc = Maxclicks.builder()
.apiKey("max_...")
.build();
Me me = mc.me();
System.out.println(me.space().slug());
// Records are flat maps with any custom attribute keys.
MaxclicksRecord contact = mc.records().upsert("students", Map.of(
"email", "[email protected]",
"firstName", "Ada",
"loyaltyTier", "gold"));
System.out.println(contact.id() + " " + contact.getString("loyaltyTier"));
Every request carries Authorization: Bearer <apiKey>, Accept: application/json, and X-Maxclicks-Client: maxclicks-java/<version>. The public form and email endpoints (forms().submit, forms().confirmDoubleOptIn, emails().unsubscribeOneClick) send no auth header.
Configuration
| Option | Default | Description |
|---|---|---|
apiKey | MAXCLICKS_API_KEY env | Bearer API key. |
baseUrl | https://api.maxclicks.ai/v1 | Override with Maxclicks.STAGE_BASE_URL for staging. |
timeout | 60s | Per-request timeout (Duration). |
maxRetries | 2 | Additional attempts on retryable failures. |
transport | JDK HttpClient | Injectable Transport for testing. |
defaultHeader | none | Extra header merged into every request. |
onWarning | none | Callback for API warnings. |
Maxclicks mc = Maxclicks.builder()
.apiKey(System.getenv("MAXCLICKS_API_KEY"))
.baseUrl(Maxclicks.STAGE_BASE_URL)
.timeout(java.time.Duration.ofSeconds(30))
.maxRetries(4)
.onWarning((warnings, ctx) -> System.err.println(ctx.path() + ": " + warnings))
.build();
Resources
The client mirrors the v1 API across sixteen groups. Management resources return typed models and take builder-style params. Contact, object, and event data is passed and returned as flat Map<String, ?>.
| Accessor | Covers |
|---|---|
mc.me() | GET /me |
mc.schemas() | list and get schemas |
mc.attributes() | list the stored and evaluated attributes on a schema |
mc.records() | create, upsert, list, get, update, delete, audit trail |
mc.suppressions() | list, create, batch create, batch delete, delete (admin key) |
mc.events() | fire, fire batch, list (cursor-paginated) |
mc.domains() | list and get domains |
mc.senders() | list sender profiles |
mc.topics() | list and get communication topics |
mc.segments() | list, create, get, delete, count, contacts |
mc.broadcasts() | list, get, update, send, runs, metrics |
mc.templates() | list, get, send |
mc.webhooks() | webhooks and secret rotation |
mc.workflows() | list, get, trigger, pause, unpause, runs, get run |
mc.forms(), mc.emails() | public form submit and one-click unsubscribe |
Several groups read but do not create. Schemas, attributes, domains, senders and topics are authored in the maxclicks app, as are templates and broadcasts (which Java can still send, and a broadcast it can still update): a schema has no migration path once records land on it, a template or broadcast needs the app's rendered preview before a send is safe, and registering a domain reserves a name globally and provisions real sending infrastructure. CSV import stays in the app for the same reason a record batch endpoint does not exist: batching collapses the per-record rate limit and auto-fill credit check into one check. Configure the entity once in the app, then drive it from Java with the send, trigger and read methods below.
mc.records().create(schema, input); // POST .../records
mc.records().upsert(schema, input); // POST .../records/upsert
mc.records().get(schema, id); // GET .../records/{id}
mc.templates().send(templateId, data); // POST /templates/{id}/send
mc.events().fire(schema, input); // POST /events/{schema}
mc.workflows().trigger(reference, body); // POST /workflows/{reference}
MaxclicksRecord and Event preserve every field. Use id(), email(), and other accessors for known fields, and get(key), getString(key), or fields() for custom attributes. An explicit null in an input map clears a nullable field.
Pagination
List methods return a Page<T> that is Iterable<T> and auto-fetches subsequent pages as you iterate. Iteration stops when hasMore is false or the offset would exceed 10,000.
import ai.maxclicks.Page;
import ai.maxclicks.models.MaxclicksRecord;
Page<MaxclicksRecord> page = mc.records().list("students");
page.data(); // this page only
page.pagination(); // limit / offset / totalCount / hasMore
for (MaxclicksRecord r : mc.records().list("students")) { // all pages
System.out.println(r.email());
}
var all = mc.records().list("students").all(); // materialize everything
var emails = mc.records().list("students").stream() // lazy over all pages
.map(MaxclicksRecord::email)
.toList();
limit (1..200, default 50) and offset (0..10000, default 0) are trailing arguments on every offset-paginated list method. events().list is the exception: it is cursor-paginated and takes no offset.
events().list(...) reads ready events from the operational store using cursor pagination. It returns a CursorPage<Event> with the same iteration methods (data(), iterator(), all(), stream()), driven by an opaque nextCursor instead of an offset. The default window is 7 days.
Errors
Every failure throws an unchecked subclass of MaxclicksException. API errors carry statusCode(), code(), type(), headers(), and raw().
import ai.maxclicks.errors.*;
try {
mc.schemas().get("does-not-exist");
} catch (MaxclicksNotFoundException e) {
System.out.println(e.statusCode() + " " + e.code() + " " + e.getMessage());
} catch (MaxclicksRateLimitException e) {
System.out.println("retry after ms: " + e.retryAfterMs());
} catch (MaxclicksApiException e) {
// any other 4xx / 5xx
} catch (MaxclicksException e) {
// configuration, connection, or timeout error
}
| Exception | When |
|---|---|
MaxclicksBadRequestException | 400 |
MaxclicksAuthenticationException | 401 |
MaxclicksPermissionException | 403 |
MaxclicksNotFoundException | 404 |
MaxclicksConflictException | 409 |
MaxclicksUnprocessableEntityException | 422 |
MaxclicksRateLimitException | 429 (carries retryAfterMs()) |
MaxclicksServerException | 5xx |
MaxclicksConnectionException | transport failure (DNS, TCP, TLS) |
MaxclicksTimeoutException | request exceeded the timeout |
MaxclicksConfigurationException | programmer error, such as a missing API key |
Retries are limited to what is safe to repeat. Reads are retried on 429, 5xx, and transport or timeout errors, up to maxRetries times. A write is retried on those only when it carries an Idempotency-Key, which is the trailing idempotencyKey argument on records().create, records().upsert, suppressions().batchCreate, suppressions().batchDelete, templates().send, broadcasts().send, and workflows().trigger. A keyless write that times out is reported to you rather than sent twice. The client also retries 429 responses according to its configured retry budget. Backoff is exponential with full jitter (base ~500ms, cap ~8s), and a Retry-After header sets the delay floor.
events().fireBatch treats a 422 as a normal result parsed into
BatchFireResult, not a thrown error. templates().send returns a
TemplateSendResponse whose status is "failed" on a genuine send failure,
while pre-send validation errors throw.
Retry outcomes
A repeated write can return a recorded result, including an error. If the API reports idempotency_outcome_unknown, inspect the affected resource or run history before creating a new operation. Keep the original key and payload while you reconcile. See the idempotency guide for supported operations and retention.