> ## Documentation Index
> Fetch the complete documentation index at: https://cactal.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Assets

> Website assets: image and file kinds with exact size and MIME limits, descriptions and categories that make assets searchable, the three-step upload flow, CDN delivery with responsive image variants, and using assets in CMS content and source code.

Assets are the images and files a website serves. They can be uploaded or generated, are stored per website, and are delivered publicly from the CDN at `cdn.cactal.app`.

Generated images use the same asset records, library, CDN URLs, responsive transformations, CMS fields, and deletion behavior as uploads. See [Generate website images](/docs/guides/generate-images).

## Kinds and limits

Every asset has a `kind` that fixes its validation rules at upload time:

| Kind    | Maximum size                | Allowed MIME types                                                                                                                                                                                                                                    |
| ------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `image` | 10,485,760 bytes (10 MiB)   | `image/png`, `image/jpeg`, `image/webp`, `image/gif`, `image/avif`, `image/svg+xml`, `image/bmp`, `image/x-icon`, `image/vnd.microsoft.icon`                                                                                                          |
| `file`  | 104,857,600 bytes (100 MiB) | Documents (PDF, Office, OpenDocument, RTF, text, CSV, Markdown, JSON, vCard), fonts (WOFF2, WOFF, TTF, OTF, TTC), audio (MP3, OGG, WAV), video (MP4, WebM, QuickTime, OGG), ZIP, Lottie (`.lottie`), Rive (`.riv`), and every raster image type above |

SVG is an `image` only. An SVG is checked when it is finalized or imported: scripts, event handler attributes, `foreignObject`, DTD entities, external stylesheets, `javascript:` links, and `image`, `feImage`, or `use` references to anything other than `#ids` or `data:image/` URIs are rejected with a `validation` error that names the reason. Embed raster images as data URIs and keep icons as plain paths.

Font, Lottie, and Rive files can be registered with `mimeType: "application/octet-stream"`; Cactal normalizes the type from the filename extension. Legacy font MIME aliases are normalized to the canonical `font/woff2`, `font/woff`, `font/ttf`, or `font/otf` type.

Only `image` assets can fill CMS `image` and `gallery` fields; `file` assets fill `file` fields. Uploads with a disallowed MIME type or an oversize `byteSize` are rejected at the first step, before any bytes move.

## Descriptions and categories

Filenames are a poor index: a library of `image.png` files tells an agent nothing. Every asset therefore carries two optional, editable fields:

| Field         | Applies to       | Value                                                                                             |
| ------------- | ---------------- | ------------------------------------------------------------------------------------------------- |
| `description` | images and files | One or two sentences, at most 500 characters: subject, visible text verbatim, style, intended use |
| `category`    | images only      | `logo`, `icon`, `photo`, `illustration`, `screenshot`, `graphic`, `background`, or `other`        |

The category says what the image is, never where it is used. Uses such as hero, thumbnail, or avatar belong in the description because one asset serves many placements. `category` on a `file` asset is rejected.

Supply both fields when you begin an upload. Agents uploading through the API or MCP have the context to write them and should. Generated images store their prompt as the description and accept an optional `category` per item. A PNG, JPEG, WebP, GIF, AVIF, BMP, or SVG image finalized without a description gets one, plus a category, written by a model in the background; that is best effort, never blocks the upload, never overwrites a description that already exists, and stops after 500 such descriptions per organization in a calendar month. An SVG is described through its rasterized preview; ICO images are not described automatically.

Both fields can be changed later with `PATCH /v1/websiteAssets/{assetId}`, which also renames the asset. Renaming never changes a delivery URL.

## Colors

Every finalized image also gets a color palette, computed once from the original bytes and returned as `colors` on list rows and single-asset reads, with `transparentShare` and `translucentShare` beside it. Each entry is `{ hex, share, accent }`: up to eight colors sorted by the fraction of opaque pixels they cover. Flat colors that a designer chose, such as UI backgrounds, text, buttons, logo fills, and illustration shapes, are reported as their exact value, while photographs are summarized by cluster centroids. `accent: true` marks saturated colors that matter even at a small share, so a brand blue on one button is not lost behind a white page.

```json theme={null}
{
  "colors": [
    { "hex": "#FFFFFF", "share": 0.723, "accent": false },
    { "hex": "#F9FAFB", "share": 0.237, "accent": false },
    { "hex": "#2563EB", "share": 0.014, "accent": true }
  ],
  "transparentShare": 0,
  "translucentShare": 0
}
```

`transparentShare` is how much of the backdrop shows through, weighting each pixel by its transparency: `0` for an opaque image, roughly the background area for a cutout, `0.5` for a uniform 50 percent overlay. `translucentShare` is the fraction of pixels with partial alpha, which flags baked soft shadows, glows, fades, and glass that will look different on a dark surface. A painted checkerboard is opaque and reports `0` for both.

SVG assets are read rather than rendered, so their `share` is `null`. Images that cannot be decoded return `colors: null`, and `file` assets never carry a palette. Agents receive the same palette wherever they see the image, including attachments and the project context index, so they reproduce a reference's exact colors instead of estimating them.

## Upload flow

Uploading is three requests: register the upload, send the bytes, finalize.

<Steps>
  <Step title="Begin the upload">
    `POST /v1/websiteAssets` with the file's metadata and, ideally, a description and category:

    ```bash theme={null}
    curl -X POST "https://api.cactal.ai/v1/websiteAssets" \
      -H "Authorization: Bearer $CACTAL_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "websiteId": "V1StGXR8_Z5jdHi6B-myT",
        "kind": "image",
        "filename": "hero.png",
        "mimeType": "image/png",
        "byteSize": 482113,
        "description": "Roastery floor at golden hour with the copper drum in front, for the homepage hero",
        "category": "photo"
      }'
    ```

    The response contains the pending asset's id and a presigned upload target:

    ```json theme={null}
    {
      "assetId": "aB3xK9mQpR7sTn2vWc5Yd",
      "upload": { "url": "https://...", "method": "PUT", "headers": { "Content-Type": "image/png" }, "expiresAt": "2026-07-08T10:15:00.000Z" }
    }
    ```
  </Step>

  <Step title="PUT the bytes">
    Send the raw bytes to `upload.url` with `upload.method` and `upload.headers`. The URL expires 15 minutes after it is issued; if it lapses, begin a new upload.

    ```bash theme={null}
    curl -X PUT "$UPLOAD_URL" \
      -H "Content-Type: image/png" \
      --data-binary @hero.png
    ```
  </Step>

  <Step title="Finalize">
    `POST /v1/websiteAssets/{assetId}/finalize` verifies the object exists in storage and that its size matches the declared `byteSize`, then flips the status from `pending` to `ready`. For images, finalize also reads and stores the pixel dimensions and the [color palette](#colors) on the asset, and a raster image with no description gets one written in the background.
  </Step>
</Steps>

<Warning>
  An asset is unusable until finalized: only `ready` assets appear in `GET /v1/websiteAssets` and only `ready` assets can be attached to CMS fields. Finalizing before the bytes arrive fails with an upload-missing error; a size mismatch fails too — re-upload and finalize again. Abandoned `pending` uploads are swept and hard-deleted after about one hour.
</Warning>

Files that already live on the public web can be imported in one call with `POST /v1/websiteAssets/import`, which fetches up to ten media URLs server-side (30 MiB each at most), applies the same validation and metadata fields as an upload, and returns ready assets. See [Upload assets](/docs/guides/upload-assets#import-from-a-public-url).

## CDN delivery and image variants

Assets serve publicly from `cdn.cactal.app` with `Cache-Control: public, max-age=31536000, immutable` — content at an asset URL never changes, so it caches for a year.

For transformable image types (`image/png`, `image/jpeg`, `image/webp`, `image/avif`), Cactal builds responsive variants at widths 256, 640, 960, 1280, 1920, and 2560, delivered as `format=auto`, `quality=80` transforms. Single-asset reads and CMS image fields return the full delivery shape; list rows carry only `url` and `srcSet` so a page of results stays small:

```json theme={null}
{
  "originalUrl": "https://cdn.cactal.app/website-assets/aB3xK9mQpR7sTn2vWc5Yd",
  "url": "https://cdn.cactal.app/cdn-cgi/image/width=1920,quality=80,format=auto/website-assets/aB3xK9mQpR7sTn2vWc5Yd",
  "srcSet": "https://cdn.cactal.app/cdn-cgi/image/width=256,.../... 256w, ... 2560w",
  "variants": { "256": "https://...", "640": "https://...", "960": "https://...", "1280": "https://...", "1920": "https://...", "2560": "https://..." }
}
```

`url` is the 1920-width default. SVG, GIF, BMP, and icon types are not transformed; they serve the original only, with `srcSet: null` and empty `variants`. `file` assets always serve their original bytes at `url`.

### SVG previews

Image models cannot read SVG, so every SVG image also gets a rasterized PNG preview, rendered at ingest with the longest edge at 1024 pixels and stored as a hidden asset that never appears in listings. `GET /v1/websiteAssets/{assetId}` exposes it as `previewUrl`. Agents, MCP previews, `view_image`, chat attachments, generation references, and the favicon PNG fallback all read the preview while the website keeps serving the original SVG. `previewUrl` is `null` for other assets and for an SVG that could not be rendered within the time limit; such an SVG still uploads, but the agent cannot look at it.

## Transparency and background removal

Cactal's generation profiles support native transparency. Set `background: "transparent"` on the item and describe isolated artwork without a surface or shadow. Prompt wording alone does not guarantee transparency. `transparentShare` (see [Colors](#colors)) reports how much backdrop actually shows through; `0` means opaque even when the file has an alpha channel. `translucentShare` includes both soft effects and nearly opaque generated foreground pixels, so inspect the image on the intended background instead of treating that statistic alone as a defect.

`POST /v1/websiteAssets/{assetId}/removeBackground` creates a new PNG asset with the main subject isolated on a transparent background. The source must be a ready PNG, JPEG, WebP, GIF, or AVIF image and is left unchanged. The result is an ordinary asset with `sourceAssetId` pointing at the source, its own `colors` and `transparentShare`, the source category, and a description that names the source. Pass `{ "trim": true }` to crop the result to the subject bounds. Soft shadows and reflections are removed, and images without a distinct subject give poor results, so look at the result before using it; over MCP the picture comes back with the response.

An image-generation item with `"background": "transparent"` stores its native PNG directly, with `sourceAssetId: null`, without an opaque duplicate or a background-removal pass. Use the removal operation above for an existing opaque asset. See [Generate website images](/docs/guides/generate-images).

Background removal is not billed to AI credits. Each organization can create 10,000 background-removed images per calendar month.

## Using assets

**In CMS items** — attach ready assets to `image`, `gallery`, and `file` fields when creating or updating items. Image values accept `"aB3x..."`, `{ "assetId": "aB3x...", "altText": "Product dashboard" }`, or `null` to clear; galleries take ordered arrays of those; files take an asset id or `{ "assetId": "..." }`. Reads return the hydrated shape above plus `filename`, `mimeType`, `byteSize`, dimensions, and `altText`. See [CMS](/docs/concepts/cms).

**In source code**: Use `url` as an image's `src`, pass `srcSet` when present, and set `sizes` to match its rendered layout. Keep `width` and `height` or an equivalent aspect ratio to prevent layout shift. Use `originalUrl` only for downloads or intentional full-resolution access. For CSS backgrounds, use supplied `variants` with `image-set()` or select the smallest width that covers the rendered size at the intended pixel density.

**As the site favicon** — designate a ready PNG, SVG, or ICO asset as the website favicon with `PUT /v1/websites/{websiteId}/favicon`, or pick one under the site's Settings in the dashboard. An SVG favicon is linked together with its PNG preview for browsers that ignore SVG icons. See [Upload assets](/docs/guides/upload-assets) and [Site favicon](/docs/guides/customize-site-wide-files#site-favicon).

Assets are website-scoped: a CMS field can only reference assets uploaded to or generated for the same website. Reference-guided generation also accepts only ready image assets from that website.

## Listing, searching, updating, and deleting

`GET /v1/websiteAssets?websiteId=...` lists ready assets newest first as compact rows (identity, `description`, `category`, dimensions, `url`, `srcSet`), with `cursor`/`limit` pagination (default `50`, maximum `100`) and four filters that combine:

* `kind`: `image` or `file`
* `type`: the broad family derived from the MIME type, one of `image`, `video`, `audio`, `font`, or `document` (everything else)
* `category`: one image category
* `search`: each term is stemmed and prefix-matched against the filename, description, and category, so `roas` finds "roastery" and `2043` finds `IMG_2043.jpg`. Multi-word searches require every term; when nothing matches, any term is enough

`GET /v1/websiteAssets/{assetId}` returns the full record with `originalUrl`, `variants`, and `status`. Over MCP, reading an image asset also returns the picture itself so an agent can look at it.

An upload begun with `listed: false` stays out of lists until the website uses it through a source edit, a CMS field, or the favicon; it still resolves by id, keeps its stable URLs, and is never described automatically. Cactal uses this for images attached to agent messages. See [Attach project images](/docs/agents/cactal-agent#attach-project-images).

`PATCH /v1/websiteAssets/{assetId}` changes `filename`, `description`, or `category`; pass only the fields to change, and `null` to clear a description or category.

`DELETE /v1/websiteAssets/{assetId}` archives rather than destroys: the asset leaves listings and can no longer be attached to CMS fields, but existing CDN URLs and already-attached CMS values keep serving so published pages never break. Deleting is idempotent.

<Note>
  To fully retire an image, replace it in the CMS items and source files that use it, then delete it. Deletion alone does not take the bytes off the CDN.
</Note>

## Constraints

| Constraint              | Value                                                                                              |
| ----------------------- | -------------------------------------------------------------------------------------------------- |
| `image` size            | maximum 10,485,760 bytes                                                                           |
| `file` size             | maximum 104,857,600 bytes                                                                          |
| Import download size    | maximum 31,457,280 bytes (30 MiB) per URL                                                          |
| `filename` length       | 1–255 characters                                                                                   |
| `description` length    | 1–500 characters, or `null`                                                                        |
| Automatic descriptions  | 500 per organization per calendar month                                                            |
| Background removals     | 10,000 per organization per calendar month                                                         |
| `search` length         | 1–200 characters                                                                                   |
| `mimeType` length       | 0–255 characters; an empty value is accepted only for recognized font, Lottie, and Rive extensions |
| Upload URL lifetime     | 15 minutes                                                                                         |
| Pending upload lifetime | Swept after about 1 hour if never finalized                                                        |
| List page size          | default `50`, maximum `100`                                                                        |
| Cache policy            | `public, max-age=31536000, immutable`                                                              |

## Next steps

<Columns cols={2}>
  <Card title="Upload assets" icon="upload" href="/docs/guides/upload-assets">
    The full begin-PUT-finalize flow with error handling.
  </Card>

  <Card title="Generate images" icon="wand-sparkles" href="/docs/guides/generate-images">
    Create or edit website imagery and store the results in this library.
  </Card>

  <Card title="Manage CMS content" icon="database" href="/docs/guides/manage-cms-content">
    Attach images, galleries, and files to content items.
  </Card>
</Columns>
