---
title: Assets
description: Upload, list, and delete site assets — raw-body uploads, path rules, size limits, and the storage-unavailable response.
order: 5
---

# Assets

Assets are files served from `/assets/<path>` on the site's hostname: images, diagrams, downloads, logos. They belong to the site, so every version shares one namespace.

For usage guidance — filenames, caching, and referencing from markdown — see [Images and assets](/writing/assets).

All endpoints require an organization API key. `:site` accepts a slug or an id.

## List assets

```
GET /v1/sites/:site/assets
```

```bash
curl -s -H "authorization: Bearer $DOCSARY_KEY" \
  "$DOCSARY_API/v1/sites/acme/assets"
```

```json
{
  "assets": [
    {
      "path": "img/architecture.png",
      "content_type": "image/png",
      "size": 48213,
      "sha256": "9f2c1a..."
    }
  ]
}
```

Ordered by path. `size` is in bytes; `sha256` is the hash of the stored object, which lets you skip an upload whose bytes are already there.

## Upload an asset

```
POST /v1/sites/:site/assets/<path>
```

The file is the raw request body. The `content-type` header declares the file's own type and is stored verbatim.

```bash
curl -s -X POST "$DOCSARY_API/v1/sites/acme/assets/img/architecture.png" \
  -H "authorization: Bearer $DOCSARY_KEY" \
  -H "content-type: image/png" \
  --data-binary @architecture.png
```

```json
{ "path": "img/architecture.png", "size": 48213, "sha256": "9f2c1a..." }
```

`201` on success, whether the asset was new or replaced an existing one at the same path. It is live immediately.

:::warning
A missing `content-type` header stores the asset as `application/octet-stream`, and it is served back that way — browsers will download it rather than display it. Set the header explicitly.
:::

### Path rules

The path is everything after `/assets/` in the request URL, percent-decoded.

- Must match `^[a-z0-9][a-z0-9._/-]*$`, case-insensitively: letters, digits, dots, underscores, hyphens, and slashes.
- Must begin with a letter or a digit.
- May not contain `..` anywhere.

### Errors

| Response | Cause |
| --- | --- |
| `400 {"error":"bad path"}` | Path fails the pattern or contains `..` |
| `400 {"error":"malformed percent-encoding in asset path"}` | The path could not be decoded |
| `400 {"error":"empty body"}` | Zero-length body |
| `413 {"error":"asset too large (20MB cap)"}` | Body over 20 MB |
| `413` with `quota: "asset_bytes"` | The upload would cross your organization's [storage ceiling](/access#the-ceilings) |
| `503` | Asset storage is temporarily unavailable |

An overwrite is charged only the difference in size against your storage limit, so replacing a file with one the same size always fits.

Pages, search, and navigation are unaffected by a `503` here — only asset upload and asset URLs are.

## Delete an asset

```
DELETE /v1/sites/:site/assets/<path>
```

```bash
curl -s -X DELETE "$DOCSARY_API/v1/sites/acme/assets/img/architecture.png" \
  -H "authorization: Bearer $DOCSARY_KEY"
```

```json
{ "deleted": "img/architecture.png" }
```

Removes the stored file and its record, and frees the space against your storage limit. `404 {"error":"asset not found"}` if there was nothing at that path.

Nothing checks whether a page still references the asset, so a delete can leave a broken image behind. Diff `GET /assets` against your markdown before pruning.

## How assets are served

Requests to `/assets/<path>` on a docs hostname are resolved against the site bound to that host, then streamed from object storage with the stored content type and:

```
cache-control: public, max-age=31536000, immutable
```

That one-year immutable promise is made to the reader's browser and cannot be revoked by a publish. When an asset's content changes, change its path. See [Images and assets](/writing/assets#caching-and-filenames).
