Java SDK reference

This page details how the Java SDK behaves, method by method. Start with the SDK page for installation, the first request and the method list.

Client

CalmLensServerClient.builder(apiKey, projectId).build() creates a synchronous, thread-safe client. Create one client per API key, share it across threads and close it on shutdown. The JAR declares the module name com.calmlens (requires com.calmlens;).

close() makes later calls fail with sdk/client-closed and, on Java 21 and later, closes the built-in HttpClient; on Java 17 it is released by garbage collection. An httpClient you pass to the builder is never closed by the SDK.

Requests send User-Agent: calmlens-java/<version> (java/<runtime version>) and, for API requests, Accept: application/json. CalmLensServerClient.VERSION is read from the Maven manifest. toString() never includes the API key, and SignedUrl.toString() hides the URL's signature.

Request options

Request options live in com.calmlens.Models: required fields have constructors and fluent setters configure the rest. Unset options are not sent. In update options, Patch.value(value) sets a field, Patch.clear() clears it, and leaving it unset keeps it:

client.assets.update(new UpdateAsset(asset.id()).description(Patch.clear()));

Content.mimeType(...) and LensDefinition.lensType(...) set the API's type field, because type would be ambiguous among the other setters.

Assets

assets.create queues processing and returns the asset while its classification().status() is processing. assets.classify and text.classify wait for classification. text.classify sends text/plain content and names the asset text unless you set a name. assets.create(name, bytes, mimeType) is a shortcut for a SubmitAsset with Content.bytes.

MethodBehaviour
assets.deleteContentRemoves the stored bytes. The record and report stay, with content().status() removed and content().removalReason() requested.
assets.retryProcesses an asset whose classification().status() is failed again. classification().failureReason() says why it failed (see Failure reasons) and is null otherwise.
assets.explainCompletes the policy evidence behind a decision: policy().evidence() becomes true and every action stays as decided. With rerun(true) it moderates the asset again under the current policy.
assets.decideRecords a reviewer's decision with its note: new DecideAsset(id, AssetModerationDecision.BLOCKED, note). An asset still pending moderation can only be blocked or purged, and purging needs confirmPurge(true) and the assets:delete permission.
assets.deleteRemoves the record, its report and its content.
assets.batchDeletes up to 500 assets, or removes their content, in one request (see below).

Batch operations

BatchAssetsResponse queued = client.assets.batch(new BatchAssets().assetIds(ids).operation(AssetBatchOperation.DELETE));

AssetBatchOperation.REMOVE_CONTENT removes the assets' content instead. Every asset must exist and the key needs assets:delete for its folder, or nothing is queued (404 asset/not-found, 403 auth/permission-denied). The response's queued() is true once the work is queued; it runs in the background and is not retried. See POST /projects/:projectId/assets/batch.

Optional analyses

Optional analyses are turned on per request with ProcessingOptions.features, and every feature is off unless you enable it. OCR stores text in the image's content().extractedText() and adds a text moderation pass to its combined report.

SubmitAsset photo = new SubmitAsset("photo.jpg", Content.file(Path.of("photo.jpg"), "image/jpeg"))
    .processingOptions(new ProcessingOptions().features(new ProcessingFeatures()
        .ocr(new FeatureOptions(true))));
Asset asset = client.assets.classify(photo);
System.out.println(asset.content().extractedText());

Conversation context

For a reply in a thread, set authorId (your label for who wrote the content) and context (earlier posts in the conversation, oldest first) on SubmitAsset or ClassifyText. CalmLens moderates only the content itself and uses the earlier posts to read who it addresses, what it refers to and whether it quotes or reports something. See Threaded moderation.

Asset reply = client.text.classify(new ClassifyText("Same to you")
    .authorId("user-9")
    .context(new ModerationContext(List.of(
        new ModerationContextItem("Nobody asked you", "user-3"),
        new ModerationContextItem("Please keep it civil")))));

The client sends only the posts CalmLens uses: the most recent MODERATION_CONTEXT_USED_ITEMS (5) posts within MODERATION_CONTEXT_USED_CHARACTERS (2,000) characters, counted as Unicode code points. The newest post is always sent, and Models.selectModerationContextItems(items) returns the posts that would be sent. A null item list, post or text is refused with sdk/invalid-argument. The asset returns authorId() and the context() it used; context() is null once the content is removed.

Hash lists

Hash lists hold PDQ perceptual hashes that image uploads are compared with. A HashListAction.FLAG list reports matches in classification().moderation().raw().hashMatches(); a HashListAction.BLOCK list also blocks close matches, and the decision's policy().decision().source() is hash-list.

HashList list = client.hashLists.create(new CreateHashList("Removed images", HashListAction.BLOCK));
HashListEntry entry = client.hashLists.addEntry(new AddHashListEntry(list.id()).assetId(asset.id()).label("Report 1042"));
HashListEntryPage page = client.hashLists.listEntries(new ListHashListEntries(list.id()).pageSize(50));
client.hashLists.update(new UpdateHashList(list.id()).action(Patch.value(HashListAction.FLAG)).description(Patch.clear()));
client.hashLists.removeEntry(new HashListEntryId(entry.id()));

hashLists.addEntry takes a 64-character hex hash (with an optional quality) or an image assetId, whose content().image().perceptualHash() is added; an asset without one fails with 409 hash-list/asset-not-hashed. Adding a hash the list already holds returns the existing entry. hashLists.delete removes the list and every entry in it.

Folders

folders.list returns a page of items, the ancestors needed to show their paths, nextCursor and hasMore. Set parentId to open a branch (omit it for the project root), and pass nextCursor() as cursor to load another page. pageSize is 1 to 100 (default 50), and search matches names among the parent's children.

Pagination

hashLists.list and folders.list return cursor pages like assets.list. assets.iterate and assets.iterateChildren return an AutoPager, an Iterable that requests the next page only when needed. See Pagination and filtering for filters.

for (Asset asset : client.assets.iterate(new ListAssets().filter("classification.moderation.status==blocked").pageSize(100))) {
  handle(asset);
}
client.assets.iterateChildren(new ListChildren(photoId)).stream().filter(child -> "plaintext-document".equals(child.type()));

The events feed (events.list, events.pages, pageSize 1 to 100, default 50) returns every event type, oldest first; filter by type() and assetId(). Prefer webhooks in production.

Responses

Responses are immutable records whose accessors keep the API's camelCase names. An asset's state is grouped in three records:

RecordHolds
upload() (AssetUpload)Receipt of the source content.
classification() (AssetClassification)Progress, completion time, failureReason() when processing failed, detections and labels, with safety findings and decisions in classification().moderation().
content() (AssetContent)Whether CalmLens holds the content, and its format(), sizeInBytes(), checksums(), image(), extractedText(), fileMetadata() (metadata embedded in an uploaded image, such as its capture time, camera and software) and safetyHold().

Event and webhook snapshots (EventAsset) carry the processing, moderation and content statuses when the event was created. classification().moderation().categories() and score() are absent until findings are available.

Optional values are null when the API omits them, lists inside responses are immutable, and timestamps are java.time.Instant. Nested objects are typed records (AssetModeration, Checksums, AssetImage, SafetyHold, ExtractedText, FileMetadata, ImageTransformOptions and so on). Only metadata values and event fact values, where the API accepts any JSON, are untyped: they use String, Long, Double, Boolean, List and Map, so 9007199254740993 stays exact and 5 is 5L, never 5.0.

Forward compatibility

Responses from newer API versions keep working: unknown fields are ignored, and values from a fixed set are read as strings, so an unknown status or visibility keeps its raw value (status().value(), with isKnown() false) instead of failing. Known values are AssetClassificationStatus, AssetModerationStatus, AssetModerationDecision (a reviewer's decision in manualReviews()) and AssetContentStatus constants you can compare with ==.

Show content publicly only when its moderation status is ALLOWED or LIMITED. LIMITED content is delivered like allowed content, but keep it out of recommendations, trending and notifications, rank it lower or collapse it; see Limited assets. Show AGE_RESTRICTED content only behind your own age gate, through signed URLs. Don't show BLOCKED or PURGED content, or any status your SDK version doesn't know.

Uploads

Content.text(text), Content.bytes(bytes, mime), Content.file(path, mime) and Content.stream(inputStream, sizeInBytes, mime) create upload content. Files and streams are sent as they are read, with a Content-Length, so large uploads are never held in memory; bytes are sent without copying or base64 encoding. Content.stream(inputStream, mime) sends a stream of unknown size, chunked.

Multipart uploads

Send the first part with assets.create(new SubmitAsset().name(...).part(content.partNumber(1).isFinal(false))), optionally with .totalSizeInBytes(size) so CalmLens checks the total up front, then each later part with assets.uploadPart(new UploadPart(assetId, content.partNumber(n).isFinal(last))). See Multipart uploads.

Non-final parts must be at least 5 MiB, and image uploads in parts must total 10 to 20 MiB. The client refuses a part it can measure before sending it:

PartRefused with
A non-final part under 5 MiB400 upload/invalid-payload
A first image part over 20 MiB400 upload/multipart-image-too-large
A final image part that leaves the total under 10 MiB400 upload/multipart-image-too-small

CalmLens checks the totals across parts and answers with the same codes, or 400 upload/size-mismatch. CalmLens saves the queue choice when the upload starts, so parts can be sent from any client or process; UploadPart.queue(Boolean) overrides it for the final part.

Idempotency keys

Every upload carries an Idempotency-Key. The client generates one per call and reuses it for that call's retries, so a retried upload never creates a second asset. To retry safely across processes, pass your own key: 1 to 255 printable ASCII characters, without a space at either end (CalmLens refuses others with 400 idempotency/invalid-key).

client.assets.create(options, new RequestOptions().idempotencyKey(listing.id() + ":photo-1"));

Upload tokens

client.uploadTokens.sign signs a single-use token locally with your API key, without a request, for a browser upload through an upload preset that requires tokens:

SignedUploadToken signed = client.uploadTokens.sign(
    new SignUploadToken("preset_4Lk8Qw2Xm9Rt5Vb3Nc7P")
        .externalId("post-42")        // optional: the uploaded asset gets this ID
        .expiresInSeconds(600));      // optional: 1 to 3600, 600 by default
// signed.token(), signed.expiresAt() (milliseconds since the Unix epoch)

Webhooks

CalmLensServerClient.parseWebhookEvent(body, signature, timestamp, secret) verifies the exact raw body bytes, then parses them. It takes only byte[] or String bodies: never pass a body your framework has already parsed and re-serialized. See Webhooks.

In Spring, the byte[] parameter receives the raw body:

@PostMapping("/webhooks/calmlens")
ResponseEntity<Void> calmlens(@RequestBody byte[] body,
    @RequestHeader(CalmLensServerClient.WEBHOOK_SIGNATURE_HEADER) String signature,
    @RequestHeader(CalmLensServerClient.WEBHOOK_TIMESTAMP_HEADER) String timestamp) {
  WebhookEventPayload event;
  try {
    event = CalmLensServerClient.parseWebhookEvent(body, signature, timestamp, webhookSecret);
  } catch (CalmLensException invalid) {
    return ResponseEntity.status(invalid.getCode().equals(ErrorCodes.SDK_INVALID_WEBHOOK_SIGNATURE) ? 401 : 400).build();
  }
  queue.add(event); // deduplicate by event.id(), then retrieve the asset for its current state
  return ResponseEntity.noContent().build();
}

In a servlet (Jakarta or javax), read and cap the body yourself:

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
  byte[] body = request.getInputStream().readNBytes(1024 * 1024 + 1);
  if (body.length > 1024 * 1024) { response.setStatus(413); return; }
  try {
    WebhookEventPayload event = CalmLensServerClient.parseWebhookEvent(body,
        request.getHeader(CalmLensServerClient.WEBHOOK_SIGNATURE_HEADER),
        request.getHeader(CalmLensServerClient.WEBHOOK_TIMESTAMP_HEADER), webhookSecret);
    queue.add(event);
    response.setStatus(204);
  } catch (CalmLensException invalid) {
    response.setStatus(invalid.getCode().equals(ErrorCodes.SDK_INVALID_WEBHOOK_SIGNATURE) ? 401 : 400);
  }
}

A verified delivery must be a JSON object with string id, type and projectId and a numeric createdAt, and its other known fields must have the API's types; otherwise parsing fails with sdk/invalid-webhook-payload. json() holds the whole body as received, including event types your SDK version doesn't model.

HelperPurpose
parseWebhookRequest(headers, body, secret), verifyWebhookRequest(headers, body, secret)Read the signature and timestamp from the request's headers. Header names match in any case, and a header repeated in the request (a list value, as in HttpHeaders.map()) is joined with commas.
verifyWebhookSignature(body, signature, timestamp, secret)Returns a boolean instead of throwing. Overloads take nowSeconds and toleranceSeconds.
isTestWebhookEvent(event)True for test sends from the portal or API, which carry the { key: "test", value: true } fact.
createWebhookSignatureHeader(secret, timestamp, body), createWebhookHeaders(secret, body, deliveryId, timestampSeconds)Sign test deliveries; see Webhook handlers.
toWebhookEventPayload(event)Turns an event from events.list into its delivery body.

The header names are WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER and WEBHOOK_DELIVERY_HEADER, and the tolerance is WEBHOOK_SIGNATURE_TOLERANCE_SECONDS (300).

CDN

cdn.getAssetUrl(new CdnRequest(id).lens("thumb")) builds a CDN URL. CdnRequest.resource is rendered (the default), original or placeholder; only rendered takes a lens or transform.

cdn.serve streams content with the API key and returns a CdnResponse for any status but a redirect, which fails with sdk/http-error and is never followed. Close the response when done. Transport failures and HTTP 429, 502, 503 and 504 are retried like API reads; once retries run out, the last response is returned. readAllBytes(maxBytes) buffers a body up to a limit. The client's timeout bounds the wait for the response headers, and a stream read that then receives nothing for that long fails with HttpTimeoutException; pass new RequestOptions().timeout(...) to cdn.serve to change it per call. Use assets.createSignedUrl for private content in browsers.

Transforms

To transform an asset without a saved lens, pass transform on CdnRequest or SignedUrlOptions, as a selector string or as structured steps, which the SDK serializes with sorted, encoded options:

client.cdn.getAssetUrl(new CdnRequest(id).transform("format=png,width=1024"));
client.cdn.getAssetUrl(new CdnRequest(id).transform(
    Transform.image(new ImageTransformOptions().format("png").width(1024))));
client.cdn.getAssetUrl(new CdnRequest(id).transform(List.of(
    Transform.image(new ImageTransformOptions().border(new ImageBorder("#fff", 2.0))
        .gravity(Gravity.position("face")).trim(Trim.border())),
    Transform.image(new ImageTransformOptions().format("webp")))));

A selector has one path segment per step, such as fit=cover,width=640/format=webp: options are sorted key=value pairs, nested options are dotted (border.color=fff) and hex colours drop the #. A lens runs before any additional transforms, and the two are joined with / (/rendered/@thumbnail/width=320). quality takes Quality.of(80) or Quality.level("high").

redact(new Redaction().text(true)) blurs the text regions found when the image was processed before any other option applies, and personalInformation(true) blurs only those holding personal information. Text regions need OCR on the upload; see Text regions and redaction.

Viewer claims

viewerClaims(new ViewerClaims()) on CdnRequest or SignedUrlOptions sends viewer claims with a CDN request: region(...) and ageConfirmed(...). The SDK appends them as a JSON viewerClaims query parameter. A public asset whose moderation status is age-restricted is served only with ageConfirmed(true); otherwise delivery is refused with 403 viewer/age-confirmation-required, even with a signed URL or API key. Set it only where your app has already decided the viewer may see age-restricted content, and leave it unset in processing jobs. The region defaults to the country of the connection reaching CalmLens, so a proxy should forward the incoming viewer's country. Retrieving an asset returns its stored moderation results, without a delivery result for any particular viewer. See Age-restricted assets and Viewer region.

Errors

Every failure is an unchecked CalmLensException with a stable getCode(); compare it with the constants in ErrorCodes, which include every public API error code.

TypeWhenExtra information
ApiExceptionThe API answered with an error, or with a body the SDK could not read (sdk/invalid-response, sdk/response-too-large, sdk/http-error)getStatus(), getRequestId() (X-Request-Id, or the error body's requestId), getHeaders(), getRetryAfter(), getDetails()
TransportExceptionThe request could not be sent or its response could not be read (sdk/transport-error)getCause() is the original exception
RequestTimeoutExceptionA request timed out (sdk/timeout); it is a TransportException
CalmLensExceptionInvalid arguments (sdk/invalid-argument), configuration (sdk/invalid-config), upload payloads (upload/*), webhooks, interruption (sdk/cancelled, with the interrupt flag kept) and use after close() (sdk/client-closed)

isNotFound() and isRetryable() help branch on common cases. isRetryable() follows from the code and status alone, whatever the exception's class. Validation failures are reported before anything is sent, and header values containing line breaks or control characters are rejected as sdk/invalid-argument. Log getRequestId() with failures and include it when contacting support. See Error handling.

Retries and timeouts

Reads (GET, HEAD) and uploads are retried up to maxRetries times (2 by default; 0 disables retries) after network failures, timeouts, HTTP 429, 502, 503 and 504, and 409 idempotency/request-in-progress. Uploads reuse their Idempotency-Key on every retry. Other writes are never retried automatically, and uploads from Content.stream can be read only once, so they are not retried either. Repeat a write yourself only with the same idempotency key.

Before retry n (from 0), the client waits d/2 plus a random part of d/2, where d is 500 ms doubled n times, at most 8 seconds. A Retry-After in seconds or as an HTTP date replaces that wait (0 retries at once), and one over 60 seconds is returned as the error instead.

CalmLensServerClient.builder(apiKey, projectId)
    .connectTimeout(Duration.ofSeconds(10))   // opening a connection
    .timeout(Duration.ofSeconds(120))         // each API request attempt, response body included; text uploads too
    .uploadTimeout(Duration.ofMinutes(30))    // each binary upload attempt; none by default
    .maxRetries(2)
    .maxResponseBytes(32L * 1024 * 1024)      // larger API responses fail with sdk/response-too-large
    .build();

Override timeout, uploadTimeout and maxRetries for one call with RequestOptions, or for every call of a derived client that shares this one's connection and lens cache with client.withRequestOptions(new RequestOptions().timeout(Duration.ofMinutes(5))). Pass httpClient(...) to use your own java.net.http.HttpClient (proxy, TLS, executor), or transport(...) to send requests through another HTTP stack. Cancel a call by interrupting its thread: it fails with sdk/cancelled and is not retried.

Lenses

ensureLens and ensureLenses create or update versioned lens definitions. ensureLenses sends only definitions this client hasn't ensured yet, in requests of at most 100 (ENSURE_LENSES_MAX_PER_REQUEST), and concurrent calls for the same new definitions share one request.

Compared with the JavaScript SDK

The Java SDK follows the JavaScript SDK's names (assets.classify, cdn.serve, verifyWebhookSignature, createWebhookSignatureHeader(secret, timestamp, body), WEBHOOK_SIGNATURE_TOLERANCE_SECONDS, event.type()), with these differences:

  • Webhook helpers are static methods on CalmLensServerClient, and parseWebhookEvent takes the body, signature, timestamp and secret as arguments instead of an options object.
  • cdn.serve returns a closeable CdnResponse with a streamed body instead of a fetch Response.
  • Request options are builders, with Content.mimeType(...) and LensDefinition.lensType(...) for the API's type field.
  • assets.iterate, assets.iterateChildren and events.pages return Iterable pagers instead of async generators.
  • Per-call idempotency keys, deadlines and retries are passed as RequestOptions, and withRequestOptions takes the same class. Calls are cancelled by interrupting their thread.

Last updated

Signing in

Signing out

Cookies on CalmLens

We use essential cookies for sign-in and security. With your permission, functional cookies remember your preferences and Google Analytics measures website use and signups. Optional ad measurement cookies link Google ad clicks to signups. Personalised advertising stays off. Manage cookies