Java SDK
Use the Java client for typed moderation requests, events and content delivery. Java 17 or later is required.
Choose a moderation flow
Use the SDK's moderate method to wait for a decision in the request, or its create method to queue moderation and let your app continue. Text and images support both flows. Audio and video always run asynchronously, even when using moderate.
For queued content, save the returned asset ID and set up a webhook for asset.classified (decision ready) and asset.failed (processing failed). Keep content unpublished until approved. You can retrieve the asset periodically instead of receiving events. The quick start demonstrates both flows.
Install
Moderate content
Keep credentials on your server. Create one client, share it across threads and close it on shutdown. The create method queues processing; moderate waits for supported synchronous processing. Audio and video remain asynchronous.
import com.calmlens.CalmLensClient;
import com.calmlens.Models.*;
try (var client = CalmLensClient.builder(System.getenv("CALMLENS_API_KEY"), System.getenv("CALMLENS_PROJECT_ID")).build()) {
Asset asset = client.text.moderate("Hello world");
if (asset.status() == AssetStatus.APPROVED) System.out.println(asset.id());
Asset queued = client.assets.create(new SubmitAsset("photo.jpg", Content.file(java.nio.file.Path.of("photo.jpg"), "image/jpeg"))
.externalId("post-42"));
client.assets.update(new UpdateAsset(queued.id()).description(Patch.clear()));
}Publish only an explicit approved result. An approved, flagged asset may still need review. See Reading reports.
Responses are immutable records with the API's field names as accessors (asset.report().categories(), event.type()). Timestamps are Instants, and whole numbers in metadata stay exact Longs. A status or visibility added after your SDK version keeps its raw value (status().value(), status().isKnown() is false) instead of failing, so treat anything other than AssetStatus.APPROVED as not publishable.
Uploads and delivery
Request options are builders in com.calmlens.Models. Required fields have constructors; fluent setters configure optional fields. Patch.value(value) sets a field, Patch.clear() clears it, and leaving it unset preserves it.
Content.file(path, mime) and Content.stream(inputStream, sizeInBytes, mime) stream uploads without reading them into memory; Content.bytes sends a byte array as it is. For multipart uploads, CalmLens saves the queue choice when the first part arrives, so later parts can come from any client. Set totalSizeInBytes on the first part to have the total checked up front.
Every upload carries an Idempotency-Key, generated per call and reused for that call's retries, so a retry never creates a second asset. Pass your own with new RequestOptions().idempotencyKey(key) to retry across processes. See Error handling.
assets.iterate and assets.iterateChildren go through every page of assets; events.pages yields events pages while more are waiting, each with the cursor to persist.
cdn.serve streams content and returns a closeable response for any status:
try (CdnResponse response = client.cdn.serve(new CdnRequest(asset.id())
.init(new HttpOptions().headers(java.util.Map.of("Range", "bytes=0-1023"))))) {
if (response.status() == 206) response.transferTo(outputStream);
}Use signed URLs for private content in browsers.
Errors, retries and timeouts
Every failure is an unchecked CalmLensException with a stable getCode(); ErrorCodes has a constant for every public API error code.
ApiException: the API answered with an error. It carriesgetStatus(),getRequestId()(theX-Request-Idheader),getHeaders(),getRetryAfter()andgetDetails().TransportException: the request could not be sent or its response could not be read (sdk/transport-error), with the original exception as its cause.RequestTimeoutException(sdk/timeout) is one.- Invalid arguments use
sdk/invalid-argument, invalid client optionssdk/invalid-config, and upload payload problems theirupload/*codes. Nothing is sent for these.
Reads and uploads are retried twice by default after network failures, HTTP 429, 502, 503 and 504 and idempotency/request-in-progress, with jittered backoff or the Retry-After delay. Other writes are not retried. Set maxRetries(0) on the builder to disable retries.
The builder's connectTimeout (10 seconds) limits opening a connection, timeout (60 seconds) limits API responses, response bodies and idle CDN streams, and uploadTimeout (15 minutes) limits uploads so large files are not cut off. Override the upload timeout per call with RequestOptions.
Requests send User-Agent: calmlens-java/<version> (Java <runtime version>).
Webhooks
Read the request body as raw bytes and pass them to parseWebhookEvent, which verifies the signature and timestamp before parsing. Never pass a body your framework has already parsed. In Spring, declare the body as @RequestBody byte[] body; in a servlet, read request.getInputStream().
WebhookEventPayload event;
try {
event = CalmLensClient.parseWebhookEvent(body,
request.getHeader(CalmLensClient.WEBHOOK_SIGNATURE_HEADER),
request.getHeader(CalmLensClient.WEBHOOK_TIMESTAMP_HEADER),
System.getenv("CALMLENS_WEBHOOK_SECRET"));
} catch (CalmLensException invalid) {
return invalid.getCode().equals(ErrorCodes.SDK_INVALID_WEBHOOK_SIGNATURE) ? 401 : 400;
}It throws sdk/invalid-webhook-signature for a forged, tampered or stale delivery (more than WEBHOOK_SIGNATURE_TOLERANCE_SECONDS, 300, from your clock) and sdk/invalid-webhook-payload for a verified body that is not a CalmLens event. The event has id(), type(), createdAt(), projectId() and data() (severity, title, description, assetId, the asset snapshot and facts). Deduplicate by event ID, and retrieve the asset for its current state rather than trusting the snapshot. verifyWebhookSignature returns a boolean instead. See Webhooks.
Test without API calls
See Java testing for offline mock setup, request inspection and simulated failures.
Local development
Use the standalone CalmLens CLI to forward events locally, regardless of your application language.