PHP SDK reference

This page covers the PHP SDK's options and behaviour in detail. For offline tests, see PHP testing.

Client settings

CalmLens\ServerClient takes your apiKey and projectId, and optionally:

SettingDefaultEffect
baseUrlhttps://api.calmlens.com/v1The API base URL.
cdnBaseUrlhttps://cdn.calmlens.com/v1The CDN base URL.
timeout120 secondsEach API request attempt, response included.
uploadTimeoutNoneEach binary upload attempt, since files may be large. Text uploads use timeout.
connectTimeout10 secondsOpening a connection.
maxRetries2Retries after the first attempt; see Retries and idempotency.
maxResponseBytes32 MiBThe largest API response body the client reads.
httpClientA new Guzzle clientA Guzzle ClientInterface with your application's transport configuration.

The defaults are constants on ServerClient, such as ServerClient::DEFAULT_TIMEOUT. An invalid API key or project ID throws ConfigurationException with sdk/invalid-config. Requests send a calmlens-php/<version> (php/<version>) User-Agent.

Keep API keys on your server. Reuse a client within a request or worker: each client caches the lens definitions it has ensured.

Calls are synchronous. There is no separate cancellation handle; waitForDecision's timeout bounds the whole wait, and the other timeouts bound each attempt.

Options and return values

Every method takes one options array whose keys match the API's field names, such as ['assetId' => 'asset_5Gm2Rk8XqT3wLp9VbN4c']. Pass ['folderId' => null] to assets->move(), or ['parentId' => null] to folders->move(), to move to the project root.

assets->delete(), folders->delete(), hashLists->delete() and hashLists->removeEntry() return nothing. Other network methods return the decoded JSON response as an associative array.

Uploads

Supply a name and exactly one of content, part or url:

  • content holds one of plainText, base64, or data/bytes: a binary string, PHP stream resource or PSR-7 stream, with a concrete MIME type. Sized streams are sent from their current position, without loading them into memory.
  • part starts a multipart upload.
  • url submits a website, such as ['name' => 'page', 'url' => 'https://example.com'].
$file = fopen('/path/to/photo.jpg', 'rb');
try {
    $asset = $client->assets->create([
        'name' => 'photo.jpg',
        'content' => ['data' => $file, 'type' => 'image/jpeg'],
        'processingOptions' => ['keepContentAfterProcessing' => true],
        'metadata' => [['id' => 'source', 'value' => 'profile']],
    ]);
} finally {
    fclose($file);
}

Multipart uploads

Start with part in place of content, then send later parts with uploadPart():

$client->assets->uploadPart([
    'assetId' => $id,
    'part' => ['data' => $bytes, 'partNumber' => 2, 'isFinal' => true],
]);

Parts need exact byte counts. Non-final parts must be at least 5 MiB, and multipart image totals must be 10–20 MiB; pass totalSizeInBytes on the first call when you know it. CalmLens stores whether the first part was queued (create()) or waits for the decision (classify()), so later parts can come from another process. Pass queue to uploadPart() only to override it.

Conversation context

For a post in a conversation, pass authorId (your label for who wrote it) and context, the earlier posts oldest first. CalmLens moderates only the content, and reads the earlier posts to understand who it addresses and what it refers to:

$asset = $client->text->classify([
    'text' => 'Same to you',
    'authorId' => 'user-9',
    'context' => ['items' => [
        ['text' => 'You played well today', 'authorId' => 'user-1'],
        ['text' => 'Thanks!', 'authorId' => 'user-9'],
    ]],
]);

The SDK sends only the posts CalmLens uses: the 5 most recent that fit in 2,000 characters (Unicode code points), always including the newest. CalmLens\ModerationContext::select($items) applies the same rule, with the limits in ModerationContext::USED_ITEMS and ModerationContext::USED_CHARACTERS. Assets return authorId and the context used, which becomes null once the content is removed. See Threaded moderation.

Moderation results

classification.moderation.status is pending, then allowed, limited, age-restricted, blocked or purged. Treat any other value as not ready. A classification failure is not a decision to allow: while classification.status is failed, classification.failureReason says why (see Failure reasons); otherwise it is null.

Manual reviews in classification.moderation.manualReviews record their decision as one of the five decided statuses. To list only allowed assets, filter with classification.moderation.status==allowed.

Record a reviewer's decision with assets->decide(); the note is required. An asset still pending moderation can only be blocked or purged, and purging deletes the stored content:

$client->assets->decide(['assetId' => $asset['id'], 'decision' => 'blocked', 'note' => 'Spam reported by members']);
$client->assets->decide(['assetId' => $asset['id'], 'decision' => 'purged', 'note' => 'Illegal content', 'confirmPurge' => true]);

assets->batch() takes assetIds (1 to 500) and an operation, Assets::BATCH_DELETE or Assets::BATCH_REMOVE_CONTENT. It checks the list before sending and is not retried.

Hash lists

Hash lists hold PDQ hashes of images you never want again. Image uploads are compared with every list in the project: a flag list reports matches, and a block list also blocks close ones.

$list = $client->hashLists->create(['name' => 'Removed images', 'action' => 'block']);
// Add a 64-character hex PDQ hash, or the hash of an image asset that finished processing.
$entry = $client->hashLists->addEntry(['hashListId' => $list['id'], 'assetId' => $assetId, 'label' => 'Report 1042']);
$page = $client->hashLists->listEntries(['hashListId' => $list['id'], 'pageSize' => 50]);
$client->hashLists->removeEntry(['entryId' => $entry['id']]);

Adding a hash the list already holds returns the existing entry. An asset without a perceptual hash fails with 409 hash-list/asset-not-hashed (ErrorCodes::HASH_LIST_ASSET_NOT_HASHED). update() takes name, description (null clears it) and action. Deleting a list deletes its entries.

Pagination and filters

Every list returns a cursor page with items, nextCursor and hasMore. Pass a page's nextCursor as cursor for the next one, and treat it as opaque. Lists take pageSize (1 to 100, 50 by default).

  • Asset lists and child lists also take ordering, such as [['field' => 'createdAt', 'direction' => 'desc']] with lowercase asc or desc, and filter; see Pagination and filtering.
  • hashLists->listEntries() returns entries newest first.
  • folders->list() is ordered by name or createdAt, and returns the ancestors needed to show the folders' paths.
  • events->list() without cursor returns no events and a nextCursor for the current feed position. Save it to consume later events.
foreach ($client->assets->each(['pageSize' => 100]) as $asset) {
    // Process each asset.
}

assets->each() and assets->eachChild() stop at a page without a next cursor, with hasMore false or with no items; events->eachPage() and events->each() stop when hasMore is false. Rather than loop, they throw InvalidResponseException with sdk/invalid-response (and the repeated cursor in details['cursor']) when a page with more to follow returns a cursor they already sent, without yielding that page.

Errors

Every error the SDK throws is a CalmLens\CalmLensException with:

  • errorCode: the API's error code, unchanged, or one of the SDK's own codes.
  • status: the HTTP status, or null without a response.
  • details: the API error's details object, such as validation issues. Transport errors carry no details.
  • headers and requestId: the X-Request-Id header, or the error body's requestId without it. Include it when you contact support.
  • getPrevious(): the underlying exception.

Helpers: retryAfter() (seconds from Retry-After, or null), isNotFound(), isRetryable() and header($name). Compare codes with the constants in CalmLens\ErrorCodes (with each code's HTTP status in ErrorCodes::STATUSES) and CalmLens\SdkErrorCodes, never with the message. To handle one kind of failure, catch a subclass from CalmLens\Exception:

ClassWhen
ApiException, and its subclasses BadRequestException (400), AuthenticationException (401), PermissionDeniedException (403), NotFoundException (404), ConflictException (409), RateLimitException (429) and ServerException (5xx)The API answered with an error status.
ConnectionException (sdk/transport-error), and its subclass TimeoutException (sdk/timeout)No complete response arrived, or waitForDecision had no decision in time.
InvalidResponseException (sdk/invalid-response, sdk/response-too-large)A successful response couldn't be read, or its body or declared Content-Length was larger than maxResponseBytes.
InvalidArgumentException (sdk/invalid-argument, or an upload/* code for payload checks the API also makes)An argument was rejected before sending.
ConfigurationException (sdk/invalid-config), a subclass of InvalidArgumentExceptionServerClient received an invalid setting.
WebhookVerificationException (sdk/invalid-webhook-signature, sdk/invalid-webhook-payload)A webhook delivery failed verification.

See Error handling.

Retries and idempotency

GET requests and uploads retry connection failures, timeouts, 429, 502, 503, 504 and 409 idempotency/request-in-progress, up to maxRetries times. Each upload generates one idempotency key and reuses it on every retry. Other writes are retried only when you supply an idempotencyKey. Streams that can't seek are never replayed.

Retry n (counting from 0) waits half of min(8, 0.5 × 2ⁿ) seconds plus a random part of the other half. A Retry-After header (whole seconds or an HTTP date; 0 retries at once) replaces that wait, and one over 60 seconds returns the error to you instead. cdn->serve() retries the same way and returns the last response.

Redirects are never followed, to protect credentials: a 3xx fails with sdk/http-error, its status and request ID, and isn't retried, cdn->serve() included.

Pass per-call settings in a requestOptions key: timeout, uploadTimeout, connectTimeout, maxRetries, maxResponseBytes and idempotencyKey. The API checks the key: 1 to 255 printable characters, with no space at either end. See Retrying requests safely.

CDN URLs and transforms

$url = $client->cdn->getAssetUrl([
    'assetIdentifier' => $asset['id'],
    'transform' => ['type' => 'imageTransform', 'options' => ['width' => 640, 'format' => 'webp']],
]);
$signed = $client->assets->createSignedUrl(['assetId' => $asset['id'], 'expiresInSeconds' => 900]);
$response = $client->cdn->serve(['assetIdentifier' => $asset['id']]);
// Check $response->getStatusCode(); read $response->getBody() in chunks, then close it.

URL builders accept resource (rendered, the default, original or placeholder), lens, a string or structured transform, and viewerClaims. Only rendered takes a lens or transform. placeholder is a small blurred PNG and takes neither.

A transform is a selector string or one or more structured steps. 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). Structured options are sorted, nested options are dotted (border.color) and hex colours drop the #, so the example above builds .../rendered/format=webp,width=640. See Lenses and transforms.

cdn->serve() returns a streaming PSR-7 response, HTTP error responses included. Its init option takes method (GET or HEAD) and headers, such as Range.

assets->createSignedUrl() takes resource (rendered, the default, or original), lens, transform, viewerClaims and expiresInSeconds (1 to 86400, 900 by default). Signed URLs let browsers load private content without your API key; see Signed URLs.

Viewer claims

CDN methods and createSignedUrl() accept viewerClaims with region and ageConfirmed. Region defaults to the country of the connection reaching CalmLens, and age confirmation to false. CalmLens accepts these as your application's assertions without verifying them.

The CDN refuses age-restricted content with 403 viewer/age-confirmation-required unless ageConfirmed is true, even for API-key and signed-URL requests. Pass 'viewerClaims' => ['ageConfirmed' => true] only where your app has already decided the viewer may see age-restricted content; applications that never serve it, including processing jobs, should leave it false. Private assets separately need access permission, and claims can't override blocks or safety holds.

A limited asset is delivered exactly like an allowed one; it is marked so your app can reduce its reach. Blocked assets aren't served, and purged assets have no content. For proxies, raw HTTP and caching, see Viewer claims and Viewer region.

Lenses

$client->ensureLens($definition) creates or updates one lens definition (identifier, name, type, version, transforms and so on). $client->ensureLenses(['lenses' => [...]]) takes several, sends them in batches of at most 100 and caches the successful definitions on the client, so ensuring the same definition again makes no request. Updating a lens requires a higher version. See Versioned custom lenses.

Webhooks

CalmLens\Webhooks::parseEvent($payload, $signature, $timestamp, $secret) verifies a delivery and returns the event. It takes the exact raw body, compares the HMAC in constant time, and accepts timestamps within Webhooks::SIGNATURE_TOLERANCE_SECONDS (300) of your clock; pass toleranceSeconds to change it, where 0 accepts only the current second. Webhooks::verify() takes the same arguments and returns a boolean.

Pass null for the signature and timestamp, and headers (from getallheaders(), $_SERVER or a PSR-7 request's getHeaders()), to read them from the request. Header names match in any case, and repeated headers are joined with ,, so a signature list split across headers verifies and a repeated timestamp doesn't.

parseEvent throws sdk/invalid-webhook-payload unless the body is an object with a string id, type and projectId and a numeric createdAt, and its other known fields (severity, title, description, assetId, asset and facts) have the API's types.

Moderation sends asset.classified when classification finishes and one event per status, for automated and reviewer decisions alike: asset.allowed, asset.limited, asset.age-restricted, asset.blocked and asset.purged. asset.failed reports failed processing. Event asset snapshots carry classification.moderation.status.

Retries of one delivery repeat its x-calmlens-delivery ID (Webhooks::DELIVERY_HEADER), so use it, or the event id, to skip duplicates. Test deliveries carry the registered test: true fact, which Webhooks::isTest($event) recognises. To test your receiver, see Webhook handlers.

Upload tokens

$client->uploadTokens->sign(['uploadPresetId' => $presetId, 'externalId' => $id]) returns token and expiresAt (Unix milliseconds). Signing is local and makes no request. Tokens last 600 seconds by default; set expiresInSeconds from 1 to 3600. Issue a fresh token per upload, after authenticating and authorising the browser user. See Upload tokens.

Newer API versions

The SDK preserves response fields it doesn't know, unknown event types and fact keys, and structured fact values, so responses from a newer API keep working. Treat a status you don't recognise as not allowed.

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