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.
Current API coverage
The SDK groups Public API calls by resource. Use the API reference for request fields, response payloads, and operation semantics, and the examples below for language-specific method calls.
Pagination helpers follow the API traversal limits. An iterator or method named all stops at the traversal cap; use scoped queries when working with larger datasets.
Install
Add the SDK dependency with Maven or Gradle:
<dependency>
<groupId>ai.maxclicks</groupId>
<artifactId>maxclicks-java</artifactId>
<version>1.1.0</version>
</dependency>
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). Supported structural field changes use setup review, which checks existing values and saved consumers before applying a conversion. A template or broadcast needs its content and recipient preview checked before sending. There is no public record-batch endpoint; use individual writes and their documented identity, rate-limit and auto-fill rules. Configure the entity 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.