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 classHTTP statusWhen
AuthenticationException401Invalid or missing API key
LimitReachedException402Plan or quota limit reached
ForbiddenException403Authenticated but not permitted (suspended / abuse-blocked)
NotFoundException404Resource does not exist
ConflictException409Duplicate resource (e.g. existing email)
InvalidStateException422Resource state blocks the operation (unverified domain, blocked recipient, unpublished template)
RateLimitException429Too many requests
ValidationException400Request 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:

HeaderDescription
X-RateLimit-LimitMaximum requests per window
X-RateLimit-RemainingRequests remaining in current window
X-RateLimit-ResetSeconds until the window resets
Retries and idempotency

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

Using TypeScript?

See the TypeScript SDK reference for Node.js, Bun, and Deno integration.