JavaScript and TypeScript SDK

Use the official SDK on your server for uploads, moderation, events and delivery.

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

Install the official SDK:

npm install @calmlens/sdk

SDK methods take a single options object. The package loads no other packages at runtime and runs on Node.js 22 or later, Bun, Deno and Cloudflare Workers. Use it on your server: the client refuses to start in a web page, where visitors could read the API key.

import { CalmLensClient } from "@calmlens/sdk";

const client = new CalmLensClient({
  apiKey: process.env.CALMLENS_API_KEY!,
  projectId: process.env.CALMLENS_PROJECT_ID!, // shown on Developer → API Keys
});

// Text: waits for the decision.
const comment = await client.assets.moderate({
  name: "text",
  externalId: "comment-42",
  content: { plainText: "Great photo!", type: "text/plain" },
});
console.log(comment.status); // "approved" or "rejected"

// Images and documents: queued; resolve with a webhook or assets.retrieve().
const image = await client.assets.create({
  name: "hero.jpg",
  content: { data: file, type: "image/jpeg" },
  externalId: "post-7",
});
console.log(image.status); // "pending"

Use assets.moderate() instead of assets.create() for supported synchronous processing. Audio and video always run in the background. See How CalmLens works for what status and report.flagged mean.

Text moderation

assets.moderate() checks comments, captions, bios and messages. It accepts the same externalId, metadata, folderId and processingOptions as assets.create(), a required name, and content with MIME type text/plain. The text is classified as a plaintext-document asset and, by default, not retained afterwards.

const result = await client.assets.moderate({
  name: "text",
  externalId: `message-${message.id}`,
  metadata: [{ id: "channel", value: message.channelId }],
  content: { plainText: message.body, type: "text/plain" },
});

Use assets.create() with the same options to queue text instead of waiting.

Moderation reports

All six parent categories are present in a completed report. Narrow on status before accessing score; not-evaluated is an explicit unknown result rather than a zero score. Qualifier detail such as graphic violence is nested below its parent.

const violence = comment.report?.categories.violence;

if (violence?.status === "evaluated" || violence?.status === "partial") {
  console.log(violence.score);
}

const graphicViolence = violence?.qualifiers?.graphic;

See Report format for the complete schema, evaluation coverage and OCR merge behavior, and Policies and actions for actions.

Asset methods

MethodPurpose
assets.create(options)Queue an upload and return immediately.
assets.moderate(options)Upload and wait for classification.
assets.list(options)List, order, and filter project assets.
assets.iterate(options)Iterate over every matching asset, fetching pages as needed.
assets.retrieve({ assetId })Fetch one asset.
assets.listChildren({ assetId, ...pagination })List extracted children.
assets.iterateChildren({ assetId })Iterate over every extracted child.
assets.uploadPart({ assetId, part })Continue a sequential multipart upload.
assets.createSignedUrl({ assetId, ...options })Sign private CDN content or previews.
assets.update({ assetId, ...changes })Update asset fields.
assets.retry({ assetId })Retry an eligible processing failure.
assets.move({ assetId, folderId })Move an asset to a folder or to the project root (null).
assets.deleteContent({ assetId })Remove stored content and keep the asset record and report.
assets.delete({ assetId })Delete the asset record and its content.

Batch operations are documented in the Assets API.

Folder methods

MethodPurpose
folders.list()List all folders in the project.
folders.retrieve({ folderId })Fetch one folder by ID.
folders.create({ name, parentId? })Create a folder at root or inside a parent.
folders.update({ folderId, name? })Rename a folder.
folders.move({ folderId, parentId })Move a folder under another parent or to root (null).
folders.delete({ folderId })Delete an empty folder.
// Create a folder hierarchy
const marketing = await client.folders.create({ name: "Marketing" });
const launch = await client.folders.create({ name: "Launch", parentId: marketing.id });

// Move an asset into a folder or back to root
await client.assets.move({ assetId: asset.id, folderId: launch.id });
await client.assets.move({ assetId: asset.id, folderId: null });

// Reorganize or delete folders
await client.folders.move({ folderId: launch.id, parentId: null });
await client.folders.update({ folderId: launch.id, name: "Public Launch" });
await client.folders.delete({ folderId: launch.id });

CDN methods

client.cdn.getAssetUrl({ assetIdentifier, resource, lens, transform }) constructs a URL without making a request. client.cdn.serve({ assetIdentifier, ...options }) fetches it and sends the client's API key. It returns the response as soon as headers arrive, so the body streams and long downloads are not cut off by timeoutMs. Private CDN reads require the API key permission assets:download; use signed URLs for browsers.

Multipart input

Binary payloads accept base64, uint8Array, buffer, or data. data supports Blob, File, Response, and ReadableStream. Streamed multipart parts must provide an exact sizeInBytes. In Node.js, openAsBlob(path) from node:fs uploads a file from disk without reading it into memory. Binary uploads have no deadline unless you set uploadTimeoutMs.

The queue mode is chosen when the upload starts: CalmLens saves it and applies it when the final part arrives, so later parts can be sent by another process. Pass queue to assets.uploadPart to override it, and totalSizeInBytes to the first part when you know the file's size.

Split original bytes before base64-encoding each part. Parts are sequential, begin at 1, and only the final part may be smaller than 5 MiB.

Lenses

Use ensureLens(options) for one definition and ensureLenses({ lenses }) for a batch.

Errors and retries

Every failure is a CalmLensError with a stable code, plus status, requestId (the X-Request-Id header), headers and retryAfter when the API responded. CALMLENS_ERROR_CODES lists the codes, error.isNotFound() and error.isRetryable() classify them, and CalmLensError.isCalmLensError(error) works across copies of the SDK. Invalid arguments throw sdk/invalid-argument and invalid client options sdk/invalid-config.

The client retries reads, and creates and uploads, which always send an Idempotency-Key, after network failures, 429, 502, 503, 504 and idempotency/request-in-progress, with backoff that honours Retry-After. Set maxRetries (default 2, 0 to disable). Pass idempotencyKey to assets.create, assets.moderate, text.moderate or assets.uploadPart to retry safely across processes. See Error handling.

Webhooks

parseWebhookEvent({ payload, headers, secret }) verifies a delivery and returns the parsed event, or throws sdk/invalid-webhook-signature or sdk/invalid-webhook-payload. verifyWebhookSignature takes the same options and returns a boolean. Pass the raw request body as a string, Uint8Array, Buffer or ArrayBuffer, never parsed JSON, and the request headers (or signature and timestamp). Deliveries more than 5 minutes old are rejected.

import { parseWebhookEvent } from "@calmlens/sdk";

// Fetch API (Next.js, Workers, Bun): the raw body is request.text()
const event = await parseWebhookEvent({
  payload: await request.text(),
  headers: request.headers,
  secret: process.env.CALMLENS_WEBHOOK_SECRET!,
});

// Express: mount express.raw({ type: "application/json" }) on the route, then
// parseWebhookEvent({ payload: req.body, headers: req.headers, secret })

events.list({ after, limit }) returns { events, cursor, hasMore } with project events oldest first. Webhook connectors deliver the same events wrapped as { id, type, createdAt, projectId, data }. Called without after, it returns no events and a cursor for "now". Pass the latest cursor back as after, and keep reading while hasMore is true:

let { cursor } = await client.events.list(); // no events yet: a cursor for now

// later, and on every poll: pages() follows the cursor until no more events are waiting
for await (const page of client.events.pages({ after: cursor, limit: 100 })) {
  for (const event of page.events) console.log(event.type, event.facts);
  cursor = page.cursor;
}

Command-line tool

Use the standalone CalmLens CLI for local development. Install a platform binary, use Homebrew, or run it with npx calmlens:

# Forward events to a local webhook handler, signed like real deliveries
npx calmlens listen --forward-to http://localhost:3000/api/webhooks/calmlens --env-file .env.local

# Create events by moderating a harmless test input (counts as one upload)
npx calmlens trigger harassment

It reads CALMLENS_API_KEY, CALMLENS_PROJECT_ID and CALMLENS_WEBHOOK_SECRET from the environment, or from the file given with --env-file. Run npx calmlens --help for every option. See Local development.

Testing

Use MockCalmLensClient from @calmlens/sdk/testing for offline tests. See JavaScript testing for setup, deterministic responses and storage customisation.

Signing in

Signing out

Cookies on CalmLens

We use essential cookies to keep you signed in and the site secure. With your permission, we'll also use functional cookies to remember preferences such as your layout and documentation language, and to offer Google One Tap sign-in. We don't use analytics or advertising cookies. Manage cookies