Local development
Webhook connectors need a public HTTPS URL. During development, use the standalone calmlens CLI: it reads your project's events with your API key and forwards each one to a local URL, with the same body, headers and signature as a real webhook delivery. It works with every SDK.
Install the CLI
On macOS or Linux, install through Homebrew:
brew tap censkh/calmlens https://github.com/Censkh/CalmLens
brew install calmlensFor Windows, or without Homebrew, download the archive for your platform from CLI releases , extract it and put calmlens (or calmlens.exe) on your PATH. These executables include their runtime; Node, npm and Bun are not required. Check the archive against the release's SHA256SUMS file.
With Node.js 22 or later, you can also install with npm install -g calmlens, or run commands with npx calmlens.
Forward events locally
calmlens listen --forward-to http://localhost:3000/api/webhooks/calmlens --env-file .env.localThe CLI reads CALMLENS_API_KEY, CALMLENS_PROJECT_ID and CALMLENS_WEBHOOK_SECRET from the environment or from --env-file. Without a secret it prints a generated one to put in your app. Use --events asset.classified,asset.flagged to forward only some event types.
To create events without uploading your own content, run calmlens trigger. It moderates a harmless test input that CalmLens classifies as harassment; trigger clean sends text that should be approved. Each trigger counts as one upload.
The CLI only forwards events created after it starts, and attempts each delivery once; it doesn't use the webhook retry schedule, and its deliveries don't appear in the portal. It reads GET /, which you can also call from your own tools with client.events.list().
Read the events feed
Prefer webhook connectors in production. The events feed suits local development, scripts and backfills, and powers calmlens listen. Call it without after to get a cursor for the current time, then pass the returned cursor back to receive only newer events. When a response has hasMore: true, request again straight away.
let { cursor } = await client.events.list();
setInterval(async () => {
const page = await client.events.list({ after: cursor });
for (const event of page.events) console.log(event.type, event.asset?.status);
cursor = page.cursor;
}, 2000);