# 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](/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

Install the official SDK:

```bash
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.

```ts
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](/docs/how-it-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.

```ts
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.

```ts
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](/docs/guides/moderation-reports) for the complete schema, evaluation coverage and OCR merge behavior, and [Policies and actions](/docs/guides/moderation-policies) for actions.

## Asset methods

| Method | Purpose |
| --- | --- |
| `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](/docs/api/assets).

## Folder methods

| Method | Purpose |
| --- | --- |
| `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. |

```ts
// 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](/docs/guides/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.

```ts
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`:

```ts
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](/docs/guides/local-development) for local development. Install a platform binary, use Homebrew, or run it with `npx calmlens`:

```bash
# 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](/docs/guides/local-development).

## Testing

Use `MockCalmLensClient` from `@calmlens/sdk/testing` for offline tests. See [JavaScript testing](/docs/sdk/javascript/testing) for setup, deterministic responses and storage customisation.
