# CDN

The CalmLens CDN serves retained asset content, previews and derived versions from `https://cdn.calmlens.com/v1`. Create a signed URL with the public API to give an untrusted client time-limited access to one private resource.

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

**Authentication:** public assets need no authentication. Private assets need a signed URL, an owner's portal session, or an API key with **Download assets** (`assets:download`) in the `X-Api-Key` header. Creating a signed URL uses a project API key on `https://api.calmlens.com/v1`. See [Authentication → CDN access](/docs/guides/authentication#cdn-access).

## Endpoints

| Endpoint | Description |
| --- | --- |
| [`POST /assets/:assetId/signed-url`](#create-asset-signed-url) | Create a short-lived signed CDN URL for an asset |

## CDN URL formats

All paths are relative to `https://cdn.calmlens.com/v1`. `assetIdentifier` is the asset ID or its `externalId`.

| URL | Returns |
| --- | --- |
| [`GET /assets/:projectId/:assetIdentifier/content`](/docs/api/cdn#original-content) | The original stored file. |
| [`GET /assets/:projectId/:assetIdentifier/preview`](/docs/api/cdn#previews) | A browser-friendly preview. HEIC and HEIF images are converted to WebP. |
| [`GET …/content/@:lensIdentifier`](/docs/guides/lenses#named-lenses) | The content with a named [lens](/docs/guides/lenses) applied, such as `@thumbnail`. |
| [`GET …/content/imageTransform[format=png,width=1024]`](/docs/guides/lenses#direct-transforms) | The content with a [direct transform](/docs/guides/lenses#image-transform-options) applied. |
| [`GET …?exp=:expiresAt&sig=:signature`](/docs/api/cdn#create-asset-signed-url) | Any of the above, authorized by a [signed URL](#create-asset-signed-url). |

Lens and transform selectors work on `/preview` too. Only assets whose content was retained (`processingOptions.keepContentAfterProcessing: true`) and that are not rejected, failed, safety-held or removed are served. The CDN checks this on every request, including requests answered from its cache, so a change takes effect within seconds. Unavailable private assets return `404`; assets blocked by their status return [`403 asset/not-available`](/docs/api/cdn#error-asset-not-available). Invalid transform selectors return [`400 asset/invalid-transform`](/docs/api/cdn#error-asset-invalid-transform).

```bash
curl -X GET "https://cdn.calmlens.com/v1/assets/your-project-id/your-asset-id/content"
```

### Original content

Use [`GET /assets/:projectId/:assetIdentifier/content`](/docs/api/cdn#original-content) for original bytes.

### Previews

Use [`GET /assets/:projectId/:assetIdentifier/preview`](/docs/api/cdn#previews) for a browser-friendly representation.

For named lenses, one-off transform selectors, and supported image options, see [Lenses & Transformation](/docs/guides/lenses).

## CDN errors

| Status | Code | Description |
| --- | --- | --- |
| 403 | <a id="error-asset-not-available" />`asset/not-available` | The asset is blocked by its status, a safety hold, or unavailable stored content. |
| 400 | <a id="error-asset-invalid-transform" />`asset/invalid-transform` | The transform selector is malformed or names an unknown transform type. |
| 400 | <a id="error-asset-invalid-transform-options" />`asset/invalid-transform-options` | The transform options do not match the supported option schema. |

Transform processing failures can also return `500` for the availability error above.

## The signed URL object

| Field | Type | Presence | Description |
| --- | --- | --- | --- |
| `url` | `string` | Required | Signed CDN URL, including the `exp` and `sig` query parameters |
| `expiresAt` | `number` | Required | When the URL expires, in milliseconds since the Unix epoch |

## Endpoint details

### Create Asset Signed URL

Create a short-lived signed CDN URL for an asset

**Endpoint:** `POST https://api.calmlens.com/v1/assets/:assetId/signed-url`

**Authentication:** API Key

#### Example request

```bash
curl -X POST "https://api.calmlens.com/v1/assets/987fcdeb-51a2-43d7-b890-123456789abc/signed-url" \
  -H "X-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  --data '{
  "resource": "content",
  "expiresInSeconds": 900
}'
```

#### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `resource` | `content \| preview` | Yes | Which CDN resource to sign |
| `lens` | `string` | No | Lens identifier to apply, such as `thumbnail`. Do not combine with `transform`. |
| `transform` | `string` | No | Direct transform selector, such as `imageTransform[format=png,width=1024]`. Do not combine with `lens`. |
| `expiresInSeconds` | `integer` | Yes | Lifetime of the URL in seconds, from 1 second to 24 hours |

#### Response

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | `string` | Yes | Signed CDN URL, including the `exp` and `sig` query parameters |
| `expiresAt` | `number` | Yes | When the URL expires, in milliseconds since the Unix epoch |

#### Error codes

| Status | Code | Description |
| --- | --- | --- |
| 401 | <a id="post-asset-signed-url-error-auth-missing-token" />`auth/missing-token` | Authentication required. Send your project API key in the X-Api-Key header. |
| 401 | <a id="post-asset-signed-url-error-auth-invalid-api-key" />`auth/invalid-api-key` | The API key is invalid, revoked or expired. Create a key under Developer → API Keys. |
| 403 | <a id="post-asset-signed-url-error-auth-project-mismatch" />`auth/project-mismatch` | This API key belongs to a different project. Use a key created for the project that owns this resource. |
| 404 | <a id="post-asset-signed-url-error-asset-not-found" />`asset/not-found` | Asset not found |
| 404 | <a id="post-asset-signed-url-error-project-not-found" />`project/not-found` | Project not found |
