Rust SDK

Use the async Rust client, or its blocking counterpart, for typed moderation requests, events and content delivery. Rust 1.88 or later is required.

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

The Rust SDK is not yet available from crates.io.

The default features are async (the async Client) and rustls (TLS with bundled root certificates). Other features:

FeatureAdds
blockingcalmlens::blocking::Client, the same API without async
rustls-native-rootsTLS with the operating system's root certificates
native-tlsTLS through the platform library
testingcalmlens::testing::MockClient, for [dev-dependencies] only

With default-features = false, the crate contains only the models and the webhook helpers, without an HTTP client.

Moderate content

Keep credentials on your server. Create one client when your application starts and share it: clones are cheap and share connections, so store it in your application state rather than creating one per request.

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

#[tokio::main]
async fn main() -> calmlens::Result<()> {
    let client = Client::new("your-api-key", "your-project-id")?;
    let asset = client.assets().moderate(SubmitAsset::text("Hello world")).await?;
    if asset.status == Some(AssetStatus::Approved) {
        println!("{}", asset.id);
    }
    let queued = client
        .assets()
        .create(SubmitAsset::new("note.txt", Content::bytes(b"Hello".to_vec(), "text/plain")))
        .await?;
    client
        .assets()
        .update(UpdateAsset { asset_id: queued.id, description: Patch::Null, ..Default::default() })
        .await?;
    Ok(())
}

Publish only an explicit approved result. An approved, flagged asset may still need review. See Reading reports.

AssetStatus and Visibility have an Unknown variant, and every response model keeps fields this SDK version doesn't know in extra, so new API values never fail decoding. Treat an unknown status as not approved.

Blocking applications

Enable the blocking feature for calmlens::blocking::Client. It runs the async client on a private runtime, so you can create and drop it anywhere, including inside an async runtime. Its calls block the current thread, so don't call them from async code; use the async client there.

use calmlens::{blocking::Client, models::*};

fn main() -> calmlens::Result<()> {
    let client = Client::new("your-api-key", "your-project-id")?;
    for asset in client.assets().iterate(ListAssets::default()) {
        println!("{}", asset?.id);
    }
    Ok(())
}

Pagination and events

assets().iterate, assets().iterate_children and events().pages fetch the next page as you read. The async client returns a Stream; the blocking client returns an Iterator.

use futures_util::TryStreamExt;

let mut assets = client.assets().iterate(ListAssets { page_size: Some(100), ..Default::default() });
while let Some(asset) = assets.try_next().await? {
    println!("{}", asset.id);
}

events().pages(ListEvents { after: Some(cursor), .. }) follows each page's cursor until a page has has_more: false. Store the last page's cursor and continue from it on the next poll. Without after, the first page is empty and its cursor marks the current time.

Uploads and delivery

Content::bytes, Content::text, Content::reader(reader, size, mime_type), Content::stream(stream, size, mime_type) and Content::file(path, mime_type) create upload content. Readers, streams and files are sent as they are read, so the whole file never needs to be in memory.

For a multipart upload, send the first part with SubmitAsset::multipart(name, content.part(1, is_final)) and later parts with assets().upload_part. Set total_size_in_bytes on the first request when you know the file's size. CalmLens saves the queue choice when the upload starts, so any process that knows the asset ID can send the remaining parts. See Multipart uploads.

cdn().serve(CdnRequest) returns a streaming CdnResponse whatever its status. Check status(), then read it with chunk(), into_stream() or bytes(); the blocking response implements std::io::Read. Drop an unfinished response to close it. Use signed URLs for private content in browsers.

Retries and idempotency

The client retries GET requests, uploads and multipart parts up to twice after network failures, timeouts and 429, 502, 503 and 504 responses, and while a request with the same idempotency key is still in progress. It waits for the Retry-After delay when the API sends one, and otherwise for a short random backoff. Configure it with Client::builder(key, project).max_retries(n); 0 turns retries off.

Each upload and part is sent with an Idempotency-Key, and the client reuses it for its own retries, so a retried upload never creates a second asset. Set idempotency_key on SubmitAsset or UploadPart to retry safely across processes. Uploads from readers and streams are sent once. See Retrying requests safely.

Errors and timeouts

Every call returns calmlens::Error:

MethodReturns
code()An ErrorCode, such as ErrorCode::AssetNotFound, or ErrorCode::Other for codes this SDK version doesn't know. Compare with a string: error.code() == "asset/not-found"
status()The HTTP status of an API error
request_id()The response's X-Request-Id; include it when you contact support
retry_after()The Retry-After delay of a 429 or in-progress response
details(), headers(), source()The API's details, the response headers and the underlying error
is_not_found(), is_retryable(), is_timeout()Common checks

Network failures use sdk/transport-error and timeouts sdk/timeout. Invalid client options return sdk/invalid-config, and invalid method arguments, such as an empty asset ID, sdk/invalid-argument. Upload content is checked before it's sent and uses the API's codes, such as upload/invalid-payload.

API calls time out after 60 seconds by default (timeout). Uploads and CDN downloads have no overall limit, so large files are not cut off; instead, every request fails if no data arrives for 60 seconds (read_timeout) or a connection takes longer than 10 seconds (connect_timeout). Use client.with_timeout(duration) to bound one call, including an upload. Pass your own reqwest::Client with ClientBuilder::http_client for a proxy or custom certificates.

Webhooks

Verify the raw request body before parsing it. construct_webhook_event(raw_body, &headers, secret) checks the signature and the timestamp (within WEBHOOK_SIGNATURE_TOLERANCE_SECONDS, 300 seconds) and returns a models::WebhookEventPayload. A missing, wrong or stale signature returns sdk/invalid-webhook-signature; a body that isn't a delivery returns sdk/invalid-webhook-payload.

With axum, take the body as Bytes, never a parsed Json extractor:

use axum::{body::Bytes, http::{HeaderMap, StatusCode}};

async fn calmlens_webhook(headers: HeaderMap, body: Bytes) -> StatusCode {
    let secret = std::env::var("CALMLENS_WEBHOOK_SECRET").unwrap_or_default();
    match calmlens::construct_webhook_event(&body, &headers, &secret) {
        Ok(event) => {
            // Queue work keyed by event.id, then respond quickly.
            println!("{} {:?}", event.event_type, event.data.asset_id);
            StatusCode::NO_CONTENT
        }
        Err(error) if error.code() == "sdk/invalid-webhook-signature" => StatusCode::UNAUTHORIZED,
        Err(_) => StatusCode::BAD_REQUEST,
    }
}

With actix-web, take the body as web::Bytes and pass the header values to construct_webhook_event_at:

use actix_web::{web, HttpRequest, HttpResponse};

async fn calmlens_webhook(request: HttpRequest, body: web::Bytes) -> HttpResponse {
    let header = |name: &str| request.headers().get(name).and_then(|v| v.to_str().ok()).unwrap_or("");
    let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs());
    let secret = std::env::var("CALMLENS_WEBHOOK_SECRET").unwrap_or_default();
    match calmlens::construct_webhook_event_at(
        &body,
        header(calmlens::WEBHOOK_SIGNATURE_HEADER),
        header(calmlens::WEBHOOK_TIMESTAMP_HEADER),
        &secret,
        now,
        calmlens::WEBHOOK_SIGNATURE_TOLERANCE_SECONDS,
    ) {
        Ok(_event) => HttpResponse::NoContent().finish(),
        Err(_) => HttpResponse::Unauthorized().finish(),
    }
}

The payload has id, event_type, created_at, project_id and data (severity, title, description, asset_id, the asset snapshot and facts). Deduplicate by event ID, and retrieve the asset for its current state rather than trusting the snapshot. verify_webhook_signature and parse_webhook_event are available separately. See Webhooks.

Test without API calls

See Rust testing for offline mock setup, request inspection and simulated failures.

Local development

Use the standalone CalmLens CLI to forward events locally, regardless of your application language.

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