---
title: Images and assets
description: Uploading images and files, referencing them from markdown, and why an asset filename should change when its contents do.
order: 4
---

# Images and assets

Images, diagrams, PDFs, and downloads are uploaded through the API and served from `/assets/<path>` on your site's hostname.

Assets belong to the **site**, not to a version. Every version of a site shares one asset namespace, so a diagram referenced from three versions is uploaded once.

## Uploading

The upload endpoint takes the file as a raw request body. The path is the URL path, given after `/assets/`:

```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": "9f2c..." }
```

:::warning
Set `content-type` explicitly. It is stored verbatim and served back on every request for that asset. When the header is missing, the asset is stored as `application/octet-stream`, and browsers will download it instead of displaying it. `curl` will not guess for you.
:::

Posting to a path that already exists replaces the object and updates its recorded size and hash.

## Referencing from markdown

```markdown
![Architecture](/assets/img/architecture.png)

*The request path, end to end.*
```

Images are fitted to the content width, so there is no width to set. A caption is a line of italic text under the image — HTML in a page renders as literal text, so `<figure>` and `<figcaption>` will not work; see [HTML in markdown](/writing/markdown#html-in-markdown).

Always reference assets by absolute path. Relative image paths are emitted verbatim and will break on any page that is not at the depth you assumed.

## Constraints

| Constraint | Value |
| --- | --- |
| Maximum size | 20 MB per asset (`413` above that) |
| Total storage | Your organization's [asset storage ceiling](/access#the-ceilings) (`413`) |
| Path characters | letters, digits, `.`, `_`, `-`, `/` |
| Path start | must begin with a letter or digit |
| Path traversal | `..` anywhere in the path is rejected |
| Empty body | rejected with `400` |

There is no image processing. No resizing, no format conversion, no thumbnail generation, no responsive `srcset`. What you upload is what is served. Compress and size images before uploading them.

## Caching and filenames

Assets are served with `cache-control: public, max-age=31536000, immutable`.

That is a one-year cache with an explicit promise that the bytes at this URL will never change. It makes repeat page loads very fast, and it has one consequence you have to design around:

:::danger
Re-uploading different content to the same asset path will not reach visitors who have already loaded it. Their browser keeps the old file for up to a year, and no publish or purge on our side changes that — the promise was made to the reader's browser, not to a cache we control.
:::

So: **when an asset's content changes, give it a new path.** The reliable pattern is a content-derived filename:

```
/assets/img/architecture-9f2c1a.png
/assets/img/architecture-4d81e7.png     ← the updated diagram
```

Update the markdown to point at the new name, push both, and delete the old asset once nothing references it. Overwriting in place is safe only for an asset that has never been served.

## Listing and deleting

```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": "9f2c..." }
  ]
}
```

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

Deleting an asset removes both the stored file and its record. Nothing checks whether a page still references it, so a delete can leave a broken image. Check `/assets` against your markdown before pruning.

Deleting a *site* removes its assets too — the records and the stored files both go, and the delete response reports how many files were removed. Nothing of yours is left behind billing storage, and nothing survives to be served afterwards. There is no undo, so [export](/api/pages#export) anything you want to keep first.
