# 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:

```bash
brew tap censkh/calmlens https://github.com/Censkh/CalmLens
brew install calmlens
```

For Windows, or without Homebrew, download the archive for your platform from [CLI releases](https://github.com/Censkh/CalmLens/releases?q=cli-v\&expanded=true), 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

```bash
calmlens listen --forward-to http://localhost:3000/api/webhooks/calmlens --env-file .env.local
```

The 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](/docs/guides/testing) 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](/docs/guides/webhooks#deliveries-and-retries), and its deliveries don't appear in the portal. It reads [`GET /projects/:projectId/events`](/docs/api/events#list-events), 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](/docs/api/events) 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.

**python**

```python
import os
from calmlens import CalmLensClient

client = CalmLensClient(
    api_key=os.environ["CALMLENS_API_KEY"],
    project_id=os.environ["CALMLENS_PROJECT_ID"],
)

import time

cursor = client.events.list()["cursor"]
while True:
    page = client.events.list({"after": cursor})
    for event in page["events"]:
        print(event["type"], (event.get("asset") or {}).get("status"))
    cursor = page["cursor"]
    if not page["hasMore"]:
        time.sleep(2)
```

**go**

```go
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"
    calmlens "github.com/Censkh/CalmLens/packages/go-sdk"
)

func main() {
    client, err := calmlens.NewClient(calmlens.Options{APIKey: os.Getenv("CALMLENS_API_KEY"), ProjectID: os.Getenv("CALMLENS_PROJECT_ID")})
    if err != nil { log.Fatal(err) }
    ctx := context.Background()
    page, err := client.Events.List(ctx, calmlens.ListEventsOptions{})
    if err != nil { log.Fatal(err) }
    cursor := page.Cursor
    for {
        page, err = client.Events.List(ctx, calmlens.ListEventsOptions{After: cursor})
        if err != nil { log.Fatal(err) }
        for _, event := range page.Events { fmt.Println(event.Type, event.Asset) }
        cursor = page.Cursor
        if !page.HasMore { time.Sleep(2 * time.Second) }
    }
}

```

**JavaScript**

```javascript
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);
```

**ruby**

```ruby
require 'calmlens'

client = CalmLens::Client.new(api_key: ENV.fetch('CALMLENS_API_KEY'), project_id: ENV.fetch('CALMLENS_PROJECT_ID'))
result = client.events.list()
puts result
client.close
```

**rust**

```rust
use calmlens::{models::*, Client};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new(std::env::var("CALMLENS_API_KEY")?, std::env::var("CALMLENS_PROJECT_ID")?)?;
    let result = client.events().list(ListEvents::default()).await?;
    println!("{result:?}");
    Ok(())
}
```

**java**

```java
import com.calmlens.CalmLensClient;
import com.calmlens.Models.*;

public class Example {
  public static void main(String[] args) {
    var client = new CalmLensClient(System.getenv("CALMLENS_API_KEY"), System.getenv("CALMLENS_PROJECT_ID"));
    var result = client.events.list(new ListEvents());
    System.out.println(result);
  }
}
```

**curl**

```sh
curl "https://api.calmlens.com/v1/projects/$CALMLENS_PROJECT_ID/events" \
  -H "X-Api-Key: $CALMLENS_API_KEY"

# Pass the returned cursor as after; continue immediately if hasMore is true.
curl "https://api.calmlens.com/v1/projects/$CALMLENS_PROJECT_ID/events?after=your-cursor" \
  -H "X-Api-Key: $CALMLENS_API_KEY"
```
