Java SDK
The official `owlat-sdk` package provides a typed client for interacting with the Owlat API from any JVM application. Requires Java 11+.
The official owlat-sdk package provides a typed client for interacting with the Owlat API from any JVM application. Requires Java 11+.
Installation
<dependency>
<groupId>com.owlat</groupId>
<artifactId>owlat-sdk</artifactId>
<version>0.1.0</version>
</dependency>
implementation 'com.owlat:owlat-sdk:0.1.0'
Quick start
import com.owlat.sdk.Owlat;
import com.owlat.sdk.model.contact.Contact;
import com.owlat.sdk.model.contact.CreateContactParams;
Owlat owlat = new Owlat("lm_live_...");
Contact contact = owlat.contacts().create(
CreateContactParams.builder("mira@acme.io")
.firstName("Mira")
.lastName("Chen")
.build()
);
You can also pass a full configuration object:
import com.owlat.sdk.OwlatConfig;
import java.time.Duration;
Owlat owlat = new Owlat(
OwlatConfig.builder("lm_live_...")
// Self-hosting? Point baseUrl at your Convex deployment site URL —
// the API is served from https://<your-deployment>.convex.site/api/v1/*
.baseUrl("https://your-deployment.convex.site")
.timeout(Duration.ofSeconds(60))
.build()
);
Resources
Contacts
Manage contacts in your audience.
// Create a contact
Contact contact = owlat.contacts().create(
CreateContactParams.builder("mira@acme.io")
.firstName("Mira")
.lastName("Chen")
.build()
);
// Get by ID or email
Contact found = owlat.contacts().get("mira@acme.io");
// Update a contact
Contact updated = owlat.contacts().update("mira@acme.io",
UpdateContactParams.builder()
.lastName("Chen-Lopez")
.build()
);
// List with cursor-based pagination
PaginatedResponse<Contact> page = owlat.contacts().list(
PaginationParams.builder()
.limit(25)
.search("jane")
.build()
);
// Fetch the next page with the returned cursor
if (!page.getPagination().isDone()) {
PaginatedResponse<Contact> next = owlat.contacts().list(
PaginationParams.builder()
.cursor(page.getPagination().getCursor())
.build()
);
}
// Or let the SDK follow cursors for you — listAll() returns a lazy
// Stream<Contact> that fetches pages on demand (limit/search honored,
// any supplied cursor ignored), so you never hold the full set in memory
owlat.contacts().listAll().forEach(c -> System.out.println(c.getEmail()));
// Delete a contact
DeleteContactResponse deleted = owlat.contacts().delete("mira@acme.io");
Transactional
Send transactional emails using pre-built templates.
import com.owlat.sdk.model.transactional.SendTransactionalParams;
import com.owlat.sdk.model.transactional.SendTransactionalResponse;
SendTransactionalResponse result = owlat.transactional().send(
SendTransactionalParams.builder("buyer@example.com")
.slug("order-confirmation")
.dataVariables(Map.of(
"orderId", "ORD-7291",
"total", "$142.00"
))
.language("en")
.build()
);
You can identify the template by slug or transactionalId. At least one must be provided.
Attachments
You can attach files using base64-encoded content or HTTPS URLs (max 10 files, 10 MB total):
import com.owlat.sdk.model.transactional.TransactionalAttachment;
SendTransactionalResponse result = owlat.transactional().send(
SendTransactionalParams.builder("buyer@example.com")
.slug("order-confirmation")
.attachment(TransactionalAttachment.builder("invoice.pdf")
.content(base64String)
.contentType("application/pdf")
.build())
.attachment(TransactionalAttachment.builder("receipt.pdf")
.url("https://your-api.com/receipts/ORD-7291.pdf")
.build())
.build()
);
Events
Send custom events to trigger automations and build segments.
import com.owlat.sdk.model.event.SendEventParams;
import com.owlat.sdk.model.event.SendEventResponse;
SendEventResponse result = owlat.events().send(
SendEventParams.builder("mira@acme.io", "plan_upgraded")
.eventProperties(Map.of(
"plan", "pro",
"mrr", 49
))
.createContactIfNotExists(true)
.build()
);
// result.getTriggeredAutomations() — number of automations triggered (int)
Topics
Manage topic memberships. The API is write-only (add/remove) — there is no
list/get endpoint, so copy the topicId from the dashboard (Topics → a topic).
import com.owlat.sdk.model.topic.*;
// Add a contact to a topic
AddToTopicResponse added = owlat.topics().addContact(
AddToTopicParams.builder("topic_abc123")
.email("mira@acme.io")
.build()
);
// added.getDoiStatus() — NOT_REQUIRED, PENDING, or CONFIRMED
// Remove a contact from a topic
RemoveFromTopicResponse removed = owlat.topics().removeContact(
new RemoveFromTopicParams("topic_abc123", "mira@acme.io")
);
Error handling
All exceptions extend OwlatException (unchecked) and include the HTTP status code plus rate limit info.
import com.owlat.sdk.exception.*;
try {
owlat.transactional().send(
SendTransactionalParams.builder("user@example.com").slug("welcome").build()
);
} catch (InvalidStateException e) {
// 422 — unverified sending domain, blocked recipient, unpublished template.
// The most common transactional failure mode.
System.err.println("Cannot send yet: " + e.getMessage());
} catch (ForbiddenException e) {
// 403 — suspended / abuse-blocked account.
System.err.println("Not permitted: " + e.getMessage());
} catch (LimitReachedException e) {
System.err.println("Plan limit reached: " + e.getMessage());
} catch (NotFoundException e) {
System.err.println("Not found: " + e.getMessage());
} catch (AuthenticationException e) {
System.err.println("Invalid API key");
} catch (RateLimitException e) {
System.err.println("Rate limited. Retry after " + e.getRetryAfter() + "s");
} catch (ValidationException e) {
System.err.println("Invalid request: " + e.getMessage());
} catch (ConflictException e) {
System.err.println("Conflict: " + e.getMessage());
} catch (OwlatException e) {
System.err.println("API error " + e.getStatusCode() + ": " + e.getMessage());
}
| Exception class | HTTP status | When |
|---|---|---|
AuthenticationException | 401 | Invalid or missing API key |
LimitReachedException | 402 | Plan or quota limit reached |
ForbiddenException | 403 | Authenticated but not permitted (suspended / abuse-blocked) |
NotFoundException | 404 | Resource does not exist |
ConflictException | 409 | Duplicate resource (e.g. existing email) |
InvalidStateException | 422 | Resource state blocks the operation (unverified domain, blocked recipient, unpublished template) |
RateLimitException | 429 | Too many requests |
ValidationException | 400 | Request body fails validation |
Every exception exposes getRateLimit() which returns a RateLimitInfo with getLimit(), getRemaining(), and getReset().
Rate limit tracking
Rate limit headers are parsed automatically from every response. Access them through getRateLimit() on any exception, or inspect the response headers directly:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests per window |
X-RateLimit-Remaining | Requests remaining in current window |
X-RateLimit-Reset | Seconds until the window resets |
The Java SDK retries transient failures automatically — up to 2 retries
(3 attempts total) with exponential backoff (500 ms base, doubling each time).
A 429 is always retried regardless of method, honoring the Retry-After
header (capped at 30 s). A 5xx or network/timeout error is retried only for
idempotent methods (GET/PUT/DELETE).
POST sends — transactional().send(...) and events().send(...) — are not
auto-retried on a 5xx or network error, since the server has no idempotency key
and a send the server already processed must not be duplicated. Those surface as
exceptions for you to handle; never blindly replay them yourself. A
RateLimitException (429) still exposes getRetryAfter() if you want to back off
and retry a call manually.
Building from source
cd packages/sdk-java
mvn compile
mvn test
See the TypeScript SDK reference for Node.js, Bun, and Deno integration.