---
title: "Upload one asset as raw bytes"
method: PUT
path: "/api/v1/sites/{slug}/assets-raw/{key}"
tags: ["Assets"]
---

# Upload one asset as raw bytes

`PUT /api/v1/sites/{slug}/assets-raw/{key}`

## Request

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

Stores one file at `key`, replacing any file already there. The request body IS the file — no JSON and no base64 — and a `Content-Length` header is required. The file's type comes from `Content-Type` (or the key's extension when it is `application/octet-stream`). Options go in the query string. 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. 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
- `key` string, required

## Query parameters

- `intent` 'hero' | 'column' | 'inline' | 'logo'
- `title` string
- `optimize` '0' | '1' | 'false' | 'true'
- `bodySha256` string

## Response `200`

An existing asset was replaced.

- AssetUploadResult
  - `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`).
  - `uploadedAt` number, required: Upload time, Unix ms.
  - `etag` string, required: Content fingerprint (MD5 hex).

## Other responses

- `201`: A new asset was created.
- `400`: The key is not allowed (`invalid_key`, `key_not_allowed`), `intent`, `title`, `optimize` or `bodySha256` is not valid (`invalid_intent`, `title_too_long`, `invalid_optimize`, `invalid_body_sha256`), the digest does not match (`body_sha256_mismatch`), 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).
- `411`: The request has no valid `Content-Length` (`length_required`).
- `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)
