Ruby SDK reference
This page lists the Ruby SDK's settings and behaviour in detail. Start with the Ruby SDK page for installation, the method list and the main workflows.
Client settings
CalmLens::ServerClient.new takes api_key: and project_id:, plus these optional settings:
| Setting | Default | Meaning |
|---|---|---|
base_url: | https://api.calmlens.com/v1 | The API base URL. |
cdn_base_url: | https://cdn.calmlens.com/v1 | The CDN base URL. |
timeout: | 120 seconds | Each socket read or write, not a whole transfer. |
open_timeout: | 10 seconds | Opening a connection. |
upload_timeout: | None | Each attempt of a binary upload as a whole. |
max_retries: | 2 | Retries of requests that are safe to repeat; see Retries and timeouts. |
retry_base_delay: | 0.5 seconds | The first retry's wait when the API gives no Retry-After. |
max_response_bytes: | 32 MiB | The largest response body the client reads. |
A URL with credentials, a query or a fragment, or one that isn't HTTP(S), raises CalmLens::ConfigurationError (sdk/invalid-config). CalmLens::VERSION holds the gem version.
Options and field names
Every option name may be snake_case or camelCase, at any depth, so options added to the API later work without an SDK update: processing_options: {keep_content_after_processing: true} is sent as processingOptions.keepContentAfterProcessing. Keys inside metadata are yours and are sent unchanged. Responses are Hashes with the API's camelCase String keys.
Request options
Every method also accepts request_options:, which applies to that call only:
| Key | Meaning |
|---|---|
idempotency_key | The Idempotency-Key to send. Uploads get a generated one when you omit it; passing one on another write makes that write retryable. |
timeout | Seconds allowed for each socket read or write. |
open_timeout | Seconds allowed to open a connection. |
upload_timeout | Seconds allowed for each attempt of a binary upload. |
max_retries | Retries for this call. |
max_response_bytes | The largest response body this call reads. |
Types
sig/ holds RBS signatures for every public method, with record types for each option Hash and response in sig/types.rbs: CalmLens::asset (with asset_classification, asset_moderation and asset_content), classification_report (categories, qualifiers, labels, media segments and features results), processing_options_input, folder, lens, event, events_page, asset_page, asset_search_response, signed_url and others. Steep and other RBS tools check your calls and the response keys you read:
asset = client.assets.classify(name: 'photo.jpg', content: {data: File.binread('photo.jpg'), type: 'image/jpeg'},
processing_options: {features: {ocr: {enabled: true}}})
asset['classification']['moderation']['status'] # CalmLens::asset_moderation_status
asset.dig('content', 'extractedText') # CalmLens::extracted_text?The signatures describe snake_case Symbol option keys; camelCase and String keys work at runtime but are not typed. Results whose fields depend on status (category, qualifier and feature results) are typed as one record with those fields optional: check status, then read score or reason. metadata values and API error details are any JSON (CalmLens::json_value).
Conversation context
assets.create, assets.classify and text.classify take author_id: and context:, the earlier posts in the conversation, oldest first; see Threaded moderation.
client.text.classify(text: 'Same to you', author_id: 'user-9',
context: {items: [{text: 'Nice photo!', author_id: 'user-1'}, {text: 'Thanks', author_id: 'user-9'}]})The SDK sends only the posts CalmLens uses: the most recent 5 (CalmLens::ModerationContext::USED_ITEMS) that fit in 2,000 characters (CalmLens::ModerationContext::USED_CHARACTERS, counted as Unicode code points). The newest post is always sent. CalmLens::ModerationContext.select_items(items) returns the posts that would be sent. The asset returns authorId and the context used; context is nil once the content is removed.
Upload content
Content is one of:
| Form | Use |
|---|---|
{type:, plain_text:} | Text. |
{type:, base64:} | Base64-encoded bytes. |
{type:, data:} | A binary String or an IO. |
An IO is streamed and never read into memory. A File, Tempfile or StringIO, or any IO given with size_in_bytes:, is sent with a Content-Length; a stream of unknown length, such as a pipe or socket, is sent with chunked transfer encoding. A stream that ends early or holds more bytes than declared fails with 400 upload/invalid-payload.
Multipart uploads start with part: on assets.create or assets.classify and continue with assets.upload_part. The queue mode chosen at the start is saved with the upload; pass queue: to upload_part only to override it. See Multipart uploads.
Reviewer decisions
assets.decide replaces the policy's decision with a reviewer's: classification.moderation.status becomes the decision, the decision and note join manualReviews, and the matching asset.<status> event is sent with a decided-by: reviewer fact. Purging needs confirm_purge: true and the assets:delete permission, and permanently deletes the stored content. A reviewer decision does not change classification.completedAt.
client.assets.decide(asset_id: asset_id, decision: 'blocked', note: 'Spam reported by three members')
client.assets.decide(asset_id: asset_id, decision: 'purged', note: 'Illegal content', confirm_purge: true)CDN URLs
cdn.get_asset_url builds a URL and cdn.serve fetches it with your API key. Both take asset_identifier: and a resource::
| Resource | Lens or transform |
|---|---|
rendered (default) | Accepted |
original | Rejected with sdk/invalid-argument |
placeholder | Rejected with sdk/invalid-argument |
cdn.get_asset_url, cdn.serve and assets.create_signed_url accept a lens:, a transform:, or both; the lens runs first. A transform is a selector string or structured steps (one Hash or an Array), serialized with sorted option keys, so equal transforms share a URL:
client.cdn.get_asset_url(asset_identifier: asset_id, transform: 'format=png,width=1024')
client.cdn.get_asset_url(asset_identifier: asset_id, transform: {type: 'imageTransform', options: {width: 1024, format: 'png'}})
# => ".../rendered/format=png,width=1024" in both casesA selector has one path segment per step, such as fit=cover,width=640/format=webp, and a lens and a transform are joined with / (/rendered/@thumbnail/width=320). Nested options are dotted (border.color) and hex colours drop the #. See Lenses and transforms.
viewer_claims:, such as {age_confirmed: true}, is added as a JSON viewerClaims query parameter after any lens or transform. Set age_confirmed only where your app has already decided the viewer may see age-restricted content; see Viewer region for the region claim and proxies.
Serving responses
cdn.serve returns a CalmLens::Response with status, headers (lowercase names), success?, request_id, content_type and content_length. With a block it yields the response with a streaming body, read with each_chunk; without one it returns the response with the body read. Read the body once, with each_chunk or body. Error statuses are returned, not raised.
Signed URLs
assets.create_signed_url(asset_id:, ...) returns {'url', 'expiresAt'}. expires_in_seconds: is 1 to 86,400 (default 900), and resource: is rendered or original. Claims passed when the URL is created are part of it, so forward the URL unchanged. See Signed URLs.
Upload tokens
client.upload_tokens.sign signs a single-use token for an upload preset locally with your API key, without a request:
signed = client.upload_tokens.sign(
upload_preset_id: 'preset_4Lk8Qw2Xm9Rt5Vb3Nc7P',
external_id: 'post-42', # optional: the uploaded asset gets this ID
expires_in_seconds: 600 # optional: 1 to 3600, 600 by default
)
# signed['token'], signed['expiresAt'] (milliseconds since the Unix epoch)Folders
folders.list returns a cursor page with items, the visible ancestors of the listed folders, nextCursor, hasMore, pageSize and ordering. Set parent_id: to open a branch (omit it for the project root), search: to list only children whose name contains it, and one ordering: entry by name (the default, ascending) or createdAt. Only empty folders can be deleted. See Pagination and filtering.
Hash lists
Add a 64-character PDQ hash, or an image asset whose hash CalmLens computed. Adding a hash already in the list returns its existing entry, and an asset without a perceptual hash fails with 409 hash-list/asset-not-hashed. See Hash lists.
list = client.hash_lists.create(name: 'Removed images', action: 'block')
entry = client.hash_lists.add_entry(hash_list_id: list['id'], asset_id: asset_id, label: 'Report 1042')
page = client.hash_lists.list_entries(hash_list_id: list['id'], page_size: 50) # items, nextCursor, hasMore
client.hash_lists.update(hash_list_id: list['id'], action: 'flag', description: nil)
client.hash_lists.remove_entry(entry_id: entry['id'])
client.hash_lists.delete(hash_list_id: list['id']) # deletes its entries tooEvents
Each event has id, projectId, type, severity, title, description, assetId, asset (the asset as it was when the event was created), facts and createdAt; description, assetId and asset are nil when the event has none. facts is an array of {'key', 'value', 'url'?} entries registered for its type; see Event facts. An unknown cursor raises 400 validation/invalid-cursor.
Webhooks
CalmLens.parse_webhook_event(payload:, secret:, ...) takes either signature: and timestamp: or the request's headers:. headers: accepts a Hash with names in any case (an Array of repeated values is joined with ,), Net::HTTPHeader or a Rack env. The header names are CalmLens::WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER and WEBHOOK_DELIVERY_HEADER.
A delivery signed more than CalmLens::WEBHOOK_SIGNATURE_TOLERANCE_SECONDS (300) from your clock fails; change the window with tolerance_seconds: on parse_webhook_event, verify_webhook_signature or verify_webhook_request.
The returned CalmLens::WebhookEvent has id, project_id, type, severity, title, description, asset_id, asset, facts, created_at and test?. to_payload returns the whole body as received, including fields CalmLens adds later. A Sinatra or Rack handler that acknowledges first and works in the background:
post '/webhooks/calmlens' do
request.body.rewind
event = CalmLens.parse_webhook_event(payload: request.body.read, headers: request.env,
secret: ENV.fetch('CALMLENS_WEBHOOK_SECRET'))
enqueue(event.id, event.to_payload) # skip an event ID you have already queued
204
rescue CalmLens::WebhookVerificationError
400
endTo build signed deliveries for tests, see Webhook handlers.
Errors
Every error is a CalmLens::Error with code, message, details, status, request_id, headers, retry_after and cause. Include request_id when you contact support. not_found? is true for a missing resource; retryable? is true for sdk/transport-error, sdk/timeout, 409 idempotency/request-in-progress, 429, 502, 503 and 504, whichever class carries them. Compare codes with the constants in CalmLens::ErrorCodes (the API's codes, with their HTTP statuses in CalmLens::ErrorCodes::STATUSES) and CalmLens::SdkErrorCodes.
| Class | When |
|---|---|
CalmLens::APIError and its subclasses BadRequestError, AuthenticationError, PermissionDeniedError, NotFoundError, ConflictError, RateLimitError, ServerError | The API answered with an error status, or with a redirect (sdk/http-error with the 3xx status), which is never followed. |
CalmLens::ConnectionError (sdk/transport-error), CalmLens::TimeoutError (sdk/timeout) | No complete response arrived, or assets.wait_for_decision had no decision in time. |
CalmLens::InvalidResponseError (sdk/invalid-response, sdk/response-too-large) | A successful response could not be read or was not what the API returns, such as an upload answered without an asset, or a response was larger than max_response_bytes:. |
CalmLens::InvalidArgumentError (sdk/invalid-argument, or an upload/* code for payload checks the API also makes) | An argument was rejected before sending. |
CalmLens::ConfigurationError (sdk/invalid-config) | ServerClient.new received an invalid setting, such as a malformed base_url:. |
CalmLens::WebhookVerificationError (sdk/invalid-webhook-signature, sdk/invalid-webhook-payload) | A webhook delivery failed verification. |
Retries and timeouts
GET and HEAD requests, and requests that carry an Idempotency-Key, are retried up to max_retries: times. Uploads always carry a key, reused on every retry, so CalmLens runs them once and replays the saved response; other writes carry one only when you pass request_options: {idempotency_key: '...'}. Connection failures, timeouts, 429, 502, 503, 504 and 409 idempotency/request-in-progress are retried with exponential backoff from retry_base_delay:, doubling up to 8 seconds, with half of each wait fixed and half random. A Retry-After in seconds or as an HTTP date replaces the backoff (0 retries at once); one over 60 seconds is raised to you instead of waited out. Redirects are never retried or followed. A streamed upload is retried only when its IO can rewind. cdn.serve retries like a read and returns the last response.
timeout: limits each socket read or write, so large downloads are not cut off. Binary uploads (file content and multipart parts) have no deadline unless you set upload_timeout:, which then limits each attempt as a whole; text content follows timeout:. A response body over max_response_bytes: fails with sdk/response-too-large, before it is read when its Content-Length says so. See Retrying requests safely.
Connections
Reuse one client. It keeps up to 8 idle keep-alive connections per host, is safe to share between threads, and never follows redirects, so your API key never reaches another host. After fork (Puma, Unicorn, Resque) the child opens its own connections instead of sharing the parent's. Call close when finished. Requests identify themselves as calmlens-ruby/<version> (ruby/<version>), and inspect never shows the API key.