# Ruby SDK

Use the Ruby client for moderation, events and content delivery. Ruby 3.1 or later is required. Options use snake\_case; responses retain API field names.

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

> The Ruby SDK is not yet available from RubyGems.

## Moderate content

Keep credentials on your server. The create method queues processing; moderate waits for supported synchronous processing. Audio and video remain asynchronous.

```ruby
require 'calmlens'
client = CalmLens::Client.new(api_key: ENV.fetch('CALMLENS_API_KEY'), project_id: ENV.fetch('CALMLENS_PROJECT_ID'))
result = client.assets.moderate(name: 'text', content: {plain_text: 'Hello world', type: 'text/plain'})
asset = client.assets.create(name: 'note.txt', content: {data: 'Hello', type: 'text/plain'})
client.close
```

Publish only an explicit approved result. An approved, flagged asset may still need review. See [Reading reports](/docs/guides/moderation-reports). Statuses and fields added to the API later are returned unchanged, so treat any status you don't recognise as not ready.

Option names may be snake\_case or camelCase at any depth, such as `processing_options: {image: {ocr_enabled: true}}`. Keys inside `metadata` are sent as you wrote them.

## Uploads and delivery

Uploads accept plain text, base64, a binary String or an IO. An IO is streamed without being read into memory. Files, Tempfiles and StringIOs know their size; for a pipe or socket, pass `size_in_bytes:`.

```ruby
File.open('clip.mp4', 'rb') do |file|
  client.assets.create(name: 'clip.mp4', content: {data: file, type: 'video/mp4'})
end
```

Multipart uploads save their queue mode when they start, so later parts can come from another process; pass `total_size_in_bytes:` at the start if you know the file size. See [Multipart uploads](/docs/guides/multipart-uploads).

Use `create_signed_url` to deliver private content to browsers. `cdn.serve` fetches content with your API key; give it a block to stream the body instead of holding it in memory:

```ruby
File.open('photo.jpg', 'wb') do |file|
  client.cdn.serve(asset_identifier: asset_id) do |response|
    raise "CDN returned #{response.status}" unless response.success?
    response.each_chunk { |chunk| file.write(chunk) }
  end
end
```

See [Delivering assets](/docs/guides/delivering-assets).

## Pagination

`assets.each`, `assets.each_child`, `events.each` and `events.each_page` fetch pages as you iterate. Without a block they return an Enumerator, so `client.assets.each(filter: 'status==approved').first(5)` requests only the pages it needs.

## Errors, retries and timeouts

Every error is a `CalmLens::Error` with `code`, `status`, `request_id` (from `X-Request-Id`), `headers`, `retry_after`, `details` and the original exception in `cause`. Subclasses let you rescue by kind: `CalmLens::NotFoundError`, `RateLimitError`, `ConflictError` and the other `APIError` classes for API responses, `ConnectionError` and `TimeoutError` when no response arrived, `InvalidArgumentError` (`sdk/invalid-argument`) and `ConfigurationError` (`sdk/invalid-config`) before anything is sent. `CalmLens::ErrorCodes` lists the API's error codes. See [Error handling](/docs/guides/error-handling).

Uploads send an `Idempotency-Key`, generated unless you pass `request_options: {idempotency_key: '...'}`, and reuse it on retries, so a retried upload never creates a second asset. Reads and uploads are retried up to `max_retries:` times (default 2) after connection failures, timeouts, `429`, `502`, `503`, `504` and [`409 idempotency/request-in-progress`](/docs/api/assets#post-asset-error-idempotency-request-in-progress), with jittered backoff that honours `Retry-After`. Other writes are not retried.

`timeout:` (default 120 seconds) limits each socket read or write rather than the whole transfer, so large uploads and downloads are not cut off. `open_timeout:` (default 10 seconds) limits connecting. Override either per call with `request_options:`.

Reuse one client: it pools keep-alive connections, is safe across threads and reconnects in forked worker processes. Call `close` when finished.

## Webhooks

Verify the exact raw body, then parse it, with `CalmLens.parse_webhook_event`. It raises `CalmLens::WebhookVerificationError` for an invalid signature or stale timestamp, and rejects an already parsed Hash.

In Rails, read `request.raw_post`:

```ruby
event = CalmLens.parse_webhook_event(
  payload: request.raw_post,
  signature: request.headers[CalmLens::WEBHOOK_SIGNATURE_HEADER],
  timestamp: request.headers[CalmLens::WEBHOOK_TIMESTAMP_HEADER],
  secret: ENV.fetch('CALMLENS_WEBHOOK_SECRET')
)
```

In Sinatra or another Rack app, read `request.body.read` and the `HTTP_X_CALMLENS_SIGNATURE` and `HTTP_X_CALMLENS_TIMESTAMP` entries of `request.env`.

Respond quickly and process the event afterwards, for example in a background job. Skip deliveries whose `x-calmlens-delivery` ID you have already handled. `CalmLens.verify_webhook_signature` returns true or false instead of raising. See [Webhooks](/docs/guides/webhooks).

## Test without API calls

See [Ruby testing](/docs/sdk/ruby/testing) for the offline mock client.

## Local development

Use the standalone [CalmLens CLI](/docs/guides/local-development) to forward events locally, regardless of your application language.
