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:

SettingDefaultMeaning
base_url:https://api.calmlens.com/v1The API base URL.
cdn_base_url:https://cdn.calmlens.com/v1The CDN base URL.
timeout:120 secondsEach socket read or write, not a whole transfer.
open_timeout:10 secondsOpening a connection.
upload_timeout:NoneEach attempt of a binary upload as a whole.
max_retries:2Retries of requests that are safe to repeat; see Retries and timeouts.
retry_base_delay:0.5 secondsThe first retry's wait when the API gives no Retry-After.
max_response_bytes:32 MiBThe 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:

KeyMeaning
idempotency_keyThe Idempotency-Key to send. Uploads get a generated one when you omit it; passing one on another write makes that write retryable.
timeoutSeconds allowed for each socket read or write.
open_timeoutSeconds allowed to open a connection.
upload_timeoutSeconds allowed for each attempt of a binary upload.
max_retriesRetries for this call.
max_response_bytesThe 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:

FormUse
{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::

ResourceLens or transform
rendered (default)Accepted
originalRejected with sdk/invalid-argument
placeholderRejected 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 cases

A 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 too

Events

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
end

To 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.

ClassWhen
CalmLens::APIError and its subclasses BadRequestError, AuthenticationError, PermissionDeniedError, NotFoundError, ConflictError, RateLimitError, ServerErrorThe 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.

Last updated

Signing in

Signing out

Cookies on CalmLens

We use essential cookies for sign-in and security. With your permission, functional cookies remember your preferences and Google Analytics measures website use and signups. Optional ad measurement cookies link Google ad clicks to signups. Personalised advertising stays off. Manage cookies