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 default features are async (the async Client) and rustls (TLS with bundled root certificates). Other features:
| Feature | Adds |
|---|---|
blocking | calmlens::blocking::Client, the same API without async |
rustls-native-roots | TLS with the operating system's root certificates |
native-tls | TLS through the platform library |
testing | calmlens::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:
| Method | Returns |
|---|---|
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.