---
title: "Upload up to 200 assets in one call"
method: POST
path: "/api/v1/sites/{slug}/assets/bulk"
tags: ["Assets"]
---

# Upload up to 200 assets in one call

`POST /api/v1/sites/{slug}/assets/bulk`

## Request

- Base URL: `https://api.dotsy.ai`
- URL: `https://api.dotsy.ai/api/v1/sites/{slug}/assets/bulk`
- Auth: HTTP bearer

Uploads 1–200 assets, each as base64 bytes, plain `bodyText` (text keys only), or an `importFromUrl`. The key is the path after `/assets/` — for example `photos/hero.webp` or `style.css`. Do not prefix it with `assets/` or `/`. Keys may not contain `..` segments or start with `.dotsy/`. A single file may be up to 5 MB on the Free plan and 10 MB on a paid plan, and video and audio need a paid plan. HTML files are not accepted. Raster images (not SVG) are optimized on upload: they are re-encoded as WebP and, for the `hero` and `column` intents, get a smaller `-800` variant, so the stored `key` can differ from the one you sent. Send `optimize: false` (`?optimize=0` on the raw upload) to store the bytes as sent; animated GIFs are always kept as sent. Items are stored in order, and the first item that fails stops the request: earlier items stay stored, and the error names the failing item in `key`. To upload one large file, send its raw bytes to `assets-raw` instead. Uploaded files are live in the editor and draft preview at once; the published site picks them up at the next publish.

## Path parameters

- `slug` string, required

## Request body

- BulkPutAssetsRequest: 1–200 assets. Each item needs exactly one of `bodyBase64`, `bodyText`, or `importFromUrl`.
  - `items` object[], required
    - `key` string, required: Path after /assets/ on the site — e.g. photos/hero.webp or style.css. Do not prefix with assets/ or /. Uploaded files are live in the editor and draft preview at once; the published site picks them up at the next publish.
    - `bodyBase64` string: The file's bytes, base64-encoded. Send this or `importFromUrl`, not both.
    - `bodyText` string: Plain UTF-8 text for text asset keys only (.css, .js, .mjs, .json, .svg, .txt, .xml). Bulk items: send this or `bodyBase64` or `importFromUrl`, not more than one.
    - `bodySha256` string: SHA-256 (64 hex characters) of the bytes you send. A mismatch answers 400 `body_sha256_mismatch` before anything is written.
    - `contentType` string: The file's MIME type. Defaults to the one implied by the key's extension.
    - `importFromUrl` string, uri: Fetch the file from this URL instead of sending `bodyBase64`. Send this or `bodyBase64`, not both.
    - `intent` 'hero' | 'column' | 'inline' | 'logo': The layout role of an image, which sets its size: `hero` (up to 2,400 px) and `column` (1,200 px) also store a smaller `-800` variant; `inline` (800 px) and `logo` (512 px) do not. Defaults to `logo` under `logos/` or `icons/`, otherwise `column`.
    - `optimize` boolean: Default true. `false` stores a raster image exactly as sent, at the key you gave, instead of re-encoding it as WebP.

## Response `200`

Every item was stored.

- BulkPutAssetsResponse
  - `ok` true, required
  - `applied` integer, required: How many items were stored.
  - `items` object[], required: One entry per item, in request order.
    - `key` string, required: The stored key. For a raster image this is the WebP key, which can differ from the key you sent.
    - `originalKey` string: The key you sent, when the image is stored under another one. `/assets/{originalKey}` keeps serving it, and published pages that reference it are pointed at the stored image. Use `key` in new references.
    - `hint` string: Present when `{name}.webp` already held another image, so this one was stored as `{name}-{ext}.webp` (in `key`). Published pages that use the name you sent still show it; CSS and JS must use `key`.
    - `created` boolean, required: True when the asset did not exist before.
    - `size` integer, required: Stored size in bytes.
    - `contentType` string, required
    - `sha256` string, required: SHA-256 of the STORED bytes. For JS, CSS and SVG it matches the upload; a raster image is re-encoded to WebP, so it may not.
    - `width` integer: Pixel width, for a processed raster image.
    - `height` integer: Pixel height, for a processed raster image.
    - `suggestedSrcset` string: A ready-made `srcset` for the image and its smaller variant.
    - `variants` object: Keys of the extra sizes stored next to the image.
      - `sm` string, required
    - `processed` boolean: True when the image was optimized on upload.
    - `skippedReencode` boolean: True when the image was already small enough to keep as is.
    - `optimized` false: Present as false when a raster image was stored exactly as sent instead of re-encoded; `reason` says why.
    - `reason` 'daily_image_budget' | 'optimize_off' | 'animated_gif': Why `optimized` is false: the daily image-processing budget was used up (`daily_image_budget`), you sent `optimize: false` (`optimize_off`), or the file is an animated GIF, which keeps its frames (`animated_gif`).
    - `etag` string, required: The stored file's content fingerprint (MD5 hex).

## Other responses

- `400`: The body is not valid (`invalid_body`), the key is not allowed (`invalid_key`, `key_not_allowed`), the bytes are not valid base64 (`invalid_base64`), the `bodySha256` does not match (`body_sha256_mismatch`), `importFromUrl` could not be fetched (`import_failed`), or the image could not be processed (`transform_failed`, `decode_failed`).
- `401`: Missing or invalid credentials.
- `403`: The Free plan does not allow video or audio (`plan_upgrade_required`), or the credentials cannot write assets: a guest token (`guest_requires_account`), a viewer role, a key scoped to other sites, or AI access below what the upload needs (`ai_access_limited`, with `aiAccess` and a `hint` — on Drafts-only sites, replacing an existing file key is refused).
- `404`: No such site, or it is not visible to this account (`site_not_found`).
- `409`: A raster's WebP names are taken: `<name>.webp` and `<name>-<ext>.webp` already hold other images (`image_name_taken`; rename the file), or a parallel upload kept changing the same WebP (`asset_write_conflict`; upload again).
- `413`: The file is over the plan's size limit (`asset_too_large`), or the site is out of storage (`storage_limit_reached`).
- `415`: The file type is not accepted, such as HTML (`asset_type_not_allowed`).

---

[API](https://skmtc.dev/dotsy/apis/dotsy-api.md) · [All operations](https://skmtc.dev/dotsy/apis/dotsy-api/llms.txt) · [OpenAPI document](https://skmtc.dev/dotsy/apis/dotsy-api/revisions/4272ed3b85d7?raw)
