Moderate assets
An asset is content submitted to CalmLens for moderation. You can create one from a file's bytes, from inline content, from a remote URL, or in sequential parts for larger uploads. This guide covers how to send each kind of content, how to wait for the decision, the limits that apply and the optional processing features. For every field and error code, see the Assets API reference.
Requests authenticate with a project API key sent from your server. See Authentication.
Upload with an SDK
For files, pass a file, buffer or stream to your SDK. It selects the upload mode and handles authentication.
import { readFile } from "node:fs/promises";
import { CalmLensServerClient } from "@calmlens/sdk/server";
const client = new CalmLensServerClient({
apiKey: process.env.CALMLENS_API_KEY!,
projectId: process.env.CALMLENS_PROJECT_ID!,
});
const asset = await client.assets.create({
name: "cat.jpg",
visibility: "private",
processingOptions: { keepContentAfterProcessing: true },
content: {
buffer: await readFile("./cat.jpg"),
type: "image/jpeg",
},
});
Waiting for the decision
Choose either flow independently of the upload mode:
| Flow | SDK | Direct HTTP | Result |
|---|---|---|---|
| Moderate in place | client.assets.moderate() | queue=false (the default) | Wait for classification and read the decision from the response. |
| Queue moderation | client.assets.create() | queue=true | Receive an asset ID while classification runs, then get the decision through an asset.classified webhook. |
Text and images support both flows. Audio and video always run asynchronously and return pending, even with queue=false. A multipart upload uses the queue mode chosen when it starts: CalmLens saves it with the upload, so whichever client sends the final part gets the same behaviour. See Queue mode.
For queued uploads, save the asset ID and configure a webhook for asset.classified and asset.failed. Keep content unpublished until approved; a successful upload alone is not a moderation decision.
Completed assets use the nested report described in Report format. Check each category's status before reading score.
See Asset lifecycle for each status and how to wait for a queued asset, and Delivering assets to serve asset content, create signed URLs for private assets, and apply lenses.
Fallback when webhooks are unavailable
If your integration cannot receive webhooks, retrieve the asset periodically with client.assets.retrieve({ assetId }). This requires a retry loop in your application; the retrieval call does not wait for classification.
Start with a short delay, increase it between checks using exponential backoff, and cap the interval. Set an overall timeout so checks cannot continue indefinitely. Stop when the status is approved, rejected or error; keep content unpublished while it is pending. If the timeout expires, treat the result as unresolved and keep the content unpublished.
client.assets.retrieve({ assetId })
Upload modes
Every upload mode uses POST /. The request body decides the mode:
| You have | Use | Request body |
|---|---|---|
| A file, buffer or stream | Direct binary upload | The file's bytes, with X-CalmLens-Upload: content |
| Base64 content or bytes inside a JSON request | JSON upload | JSON with content.base64 |
| Text you have already extracted | Plain text upload | JSON with content.plainText |
| A public web page or file URL | Remote URL upload | JSON with url |
| A large file, or one you produce in chunks | Multipart upload | The first part's bytes, with X-CalmLens-Upload: part, then POST / for the rest |
The SDKs choose the mode for you: they stream files with direct binary or multipart uploads and send other inputs as JSON.
Direct binary upload
For direct HTTP integrations, send the unencoded file as the request body with the file's Content-Type and the X-CalmLens-Upload: content header. Asset fields go in query parameters. The header is what marks the body as a file, so a JSON file is uploaded as a file rather than read as a request. See POST / for the complete request.
JSON upload
Without the X-CalmLens-Upload header, the body is JSON: base64 content, plain text or a remote URL.
- Direct image uploads must be smaller than
10MB. - All other file types support up to
5GB. - The request body can contain
content.base64,content.plainText, orurl.
JSON upload with base64 content
import { CalmLensServerClient } from "@calmlens/sdk/server";
const client = new CalmLensServerClient({
apiKey: "your-api-key",
projectId: "your-project-id"
});
const response = await client.assets.create({
"name": "cat.png",
"description": "Homepage hero image",
"visibility": "private",
"processingOptions": {
"keepContentAfterProcessing": true
},
"content": {
"base64": "your-base64-file",
"type": "image/png"
}
});
Plain text upload
import { CalmLensServerClient } from "@calmlens/sdk/server";
const client = new CalmLensServerClient({
apiKey: "your-api-key",
projectId: "your-project-id"
});
const response = await client.assets.create({
"name": "notes.txt",
"content": {
"plainText": "This is the extracted text to moderate.",
"type": "text/plain"
}
});
Remote URL upload
Remote URLs must use HTTP or HTTPS. Private IP addresses, localhost names and URL credentials are rejected, and each redirect is checked. Downloads time out after 30 seconds and are limited to 10 MB for images and 20 MB for other files. Use multipart upload for larger files.
import { CalmLensServerClient } from "@calmlens/sdk/server";
const client = new CalmLensServerClient({
apiKey: "your-api-key",
projectId: "your-project-id"
});
const response = await client.assets.create({
"name": "landing-page",
"url": "https://example.com"
});
Multipart upload
The SDK chooses multipart uploads when needed. For manual chunks, ordering, finalisation and recovery, see Multipart uploads.
Retrying uploads
Each successful upload creates an asset that counts toward your usage, and a request that timed out may still have succeeded. Send an Idempotency-Key header with a unique value, such as a UUID, and reuse it when you retry: if the first request succeeded, CalmLens returns its response instead of creating a second asset. An externalId is also unique within a project, so a retry that reuses one is refused with 409 asset/duplicate-external-id rather than creating a duplicate. See Retrying requests safely.
Asset types and limits
CalmLens detects the asset type from the content's MIME type.
| Asset type | How it is detected | Typical input | Upload limit | Guide |
|---|---|---|---|---|
image | image/* MIME types | PNG, JPEG, WebP, GIF, AVIF | 20MB | OCR and image reports |
video BETA | video/* MIME types | MP4, MOV, WebM | 5GB | Audio and video |
audio BETA | audio/* MIME types | MP3, WAV, M4A | 5GB | Audio and video |
plaintext-document | Plain text MIME types | TXT, JSON, CSV, YAML | 5GB | Reading reports |
multimedia-document | Supported document MIME types | PDF, DOCX, DOC, RTF, ODT | 5GB | Reading reports |
website | text/html MIME type or URL upload | HTML or remote website URL | 5GB | Reading reports |
file | Fallback for other file types | Other binary files | 5GB | Reading reports |
Image uploads are also constrained by a maximum image dimension of 12,000 x 12,000 pixels.
Processing features
Optional analyses are chosen per upload in processingOptions.features, keyed by feature. Each feature takes an object with enabled and any options of its own. There is no project setting for them: a feature runs for an asset only when the request that uploaded it asks for it.
| Feature | What it does | Stage | Options | Billed per image |
|---|---|---|---|---|
ocr | Reads text in images into a linked text asset and moderates it. | Generally available | enabled | OCR |
personDetection | Locates people in images. | Beta | enabled | Person detection |
faceDetection | Locates faces, with an estimated age range for each when ageEstimation is true. | Beta | enabled, ageEstimation | Face detection |
import { readFile } from "node:fs/promises";
import { CalmLensServerClient } from "@calmlens/sdk/server";
const client = new CalmLensServerClient({
apiKey: process.env.CALMLENS_API_KEY!,
projectId: process.env.CALMLENS_PROJECT_ID!,
});
const asset = await client.assets.moderate({
name: "photo.jpg",
content: { buffer: await readFile("./photo.jpg"), type: "image/jpeg" },
processingOptions: {
features: {
personDetection: { enabled: true },
faceDetection: { enabled: true, ageEstimation: true },
},
},
});
const faces = asset.report?.features?.faceDetection;
if (faces?.status === "evaluated") {
for (const face of faces.faces) console.log(face.box, face.ageRange);
}
- Every feature is opt-in. Leaving it out, or sending
enabled: false, means it doesn't run. - Unknown features and unknown options are rejected, so a typo fails the request instead of being ignored.
- Features apply to images, including images extracted from multimedia documents and websites, which inherit the parent's processing options. Other asset types ignore them.
- The features an asset was uploaded with are saved in
asset.processingOptions.features. Retrying the asset runs the same features again. - Person and face detection results are returned in
report.featuresand never change the moderation decision.
Asset metadata
Attach custom fields to your uploads. See Asset metadata for field definitions and examples.
Folders
Pass a folderId to organise an upload. See Folders for creating, moving and managing a folder tree.