Classify assets

Submit files, inline content or remote URLs for moderation, using multipart uploads for larger files. Authenticate with a project API key on your server; see the Assets API reference for fields and errors.

Upload with an SDK

For files, pass a file, buffer or stream to your SDK. It selects the upload mode and handles authentication.

How should the decision arrive?
import { readFile } from "node:fs/promises";
const asset = await client.assets.classify({
  name: "photo.jpg",
  content: { buffer: await readFile("./photo.jpg"), type: "image/jpeg" },
});
// Audio and video can still be pending; publish only approved assets.
console.log(asset.classification.moderation.decision);

Waiting for the decision

Choose either flow independently of the upload mode:

FlowSDKDirect HTTPResult
Moderate in placeclient.assets.classify()queue=false (the default)Wait for classification and read the decision from the response.
Queue moderationclient.assets.create()queue=trueReceive an asset ID while classification runs, then get the decision through an asset.approved or asset.rejected webhook.

Text and images support both flows. Audio and video always run asynchronously and return with classification.moderation.decision 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.approved, asset.rejected and asset.failed. Keep content unpublished until classification.moderation.decision is approved; a successful upload alone is not a moderation decision.

Completed assets carry classification.moderation, described in Report format. Check each category's status before reading score.

See Asset lifecycle for processing, moderation and content states, 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 }). Use client.assets.waitForDecision() for SDK polling with a timeout, or manage the polling loop yourself; a single 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 classification.moderation.decision is approved or rejected, or classification.status is failed; keep content unpublished while classification.moderation.decision 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 /projects/:projectId/assets. The request body decides the mode:

You haveUseRequest body
A file, buffer or streamDirect binary uploadThe file's bytes, with X-CalmLens-Upload: content
Base64 content or bytes inside a JSON requestJSON uploadJSON with content.base64
Text you have already extractedPlain text uploadJSON with content.plainText
A public web page or file URLRemote URL uploadJSON with url
A large file, or one you produce in chunksMultipart uploadThe first part's bytes, with X-CalmLens-Upload: part, then POST /assets/:assetId/part 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 /projects/:projectId/assets 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, or url.

JSON upload with base64 content

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

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.

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

Use an Idempotency-Key when retrying uploads; SDKs generate one automatically. See Retrying requests safely for replay behaviour and the distinction from externalId.

Asset types and limits

CalmLens detects the asset type from the content's MIME type.

Asset typeHow it is detectedTypical inputUpload limitGuide
imageimage/* MIME typesPNG, JPEG, WebP, GIF, AVIF20MBOCR and image reports
video Previewvideo/* MIME typesMP4, MOV, WebM5GBAudio and video
audio Previewaudio/* MIME typesMP3, WAV, M4A5GBAudio and video
plaintext-documentPlain text MIME typesTXT, JSON, CSV, YAML5GBReading reports
multimedia-documentSupported document MIME typesPDF, DOCX, DOC, RTF, ODT5GBReading reports
website Previewtext/html MIME type or URL uploadHTML or remote website URL5GBWebsite moderation
fileFallback for other file typesOther binary files5GBReading 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. Leaving a feature out or sending enabled: false means it does not run.

FeatureWhat it doesStageOptionsBilled per image
ocrStores extracted text on the image and adds a text moderation pass.Generally availableenabledOCR
personDetectionLocates people in images.PreviewenabledPerson detection
faceDetectionLocates faces, with an estimated age range for each when ageEstimation is true.Previewenabled, ageEstimationFace detection
import { readFile } from "node:fs/promises";

const asset = await client.assets.classify({
  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.classification.faceDetection;
if (faces?.status === "evaluated") {
  for (const face of faces.faces) console.log(face.box, face.ageRange);
}
  • 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 classification.personDetection and classification.faceDetection and 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.

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