# 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](/docs/guides/webhooks) for [`asset.classified`](/docs/api/events#event-asset-classified) (decision ready) and [`asset.failed`](/docs/api/events#event-asset-failed) (processing failed). Keep content unpublished until approved. You can retrieve the asset periodically instead of receiving events. The [quick start](/docs) demonstrates both flows.

## Install

> The Java SDK is not yet available from Maven Central.

## 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.

```java
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](/docs/guides/moderation-reports).

Responses are immutable records with the API's field names as accessors (`asset.report().categories()`, `event.type()`). Timestamps are `Instant`s, and whole numbers in metadata stay exact `Long`s. 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](/docs/guides/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](/docs/guides/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:

```java
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 carries `getStatus()`, `getRequestId()` (the `X-Request-Id` header), `getHeaders()`, `getRetryAfter()` and `getDetails()`.
- `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 options `sdk/invalid-config`, and upload payload problems their `upload/*` 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()`.

```java
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](/docs/guides/webhooks).

## Test without API calls

See [Java testing](/docs/sdk/java/testing) for offline mock setup, request inspection and simulated failures.

## Local development

Use the standalone [CalmLens CLI](/docs/guides/local-development) to forward events locally, regardless of your application language.
