# Assets and images

> Use permanent asset URLs cached for a year, and resize or convert images on request with w, h, fit, fmt and q for responsive images in your app.

Every image and file in a Content Delivery API response has an absolute, permanent URL that browsers and CDNs may cache for a year. Images can be resized and converted on request with `w`, `h`, `fit`, `fmt` and `q`. This page is for developers who show Spatibase images and files on a website or in an app.

## What does an asset URL look like?

An asset URL has the form `https://api.spatibase.com/assets/<assetId>/<filename>`. Assets appear in `images` and `files` fields ([Field values](https://spatibase.com/docs/reference/field-values/#images-and-files)) as objects like this:

```json
{"id": "b8251bf8-2fab-4001-a3a1-49465ebcabfb", "url": "https://api.spatibase.com/assets/b8251bf8-2fab-4001-a3a1-49465ebcabfb/redwood-national-park-1.jpg", "filename": "redwood-national-park-1.jpg", "mimeType": "image/jpeg", "size": 31475, "width": 1600, "height": 1000, "alt": "Redwood National Park, trailhead view"}
```

- The file route is outside `/delivery/v1` and not scoped to a project. The asset is found by its id; the filename part is cosmetic, so `/assets/<id>` or any other name returns the same file.
- `url` is always absolute. Links to the project's files inside `markdown` fields are rewritten to the same absolute form.
- A file's content never changes under its id. Editing the alt text or the filename changes the JSON, not the bytes, and old URLs with the previous filename keep working. Deleting an asset makes its URL return `404` and removes it from fields. A deleted asset stays in the trash for 30 days: restoring it brings its URL back, and once it is deleted for good the URL stays `404`.

The asset URL and its parameters are also described in both [OpenAPI files](https://spatibase.com/docs/reference/delivery-api/#machine-readable-spec) (`getAssetFile`).

## Are asset URLs public?

Yes. Asset URLs work without an API key, also in private projects, because an `<img>` tag cannot send one. Anyone with the URL can download the file, whether or not an entry using it is published; an uploaded file that is used nowhere is still served. Asset ids are long random UUIDs, but Spatibase does not sign file URLs, so do not upload anything private.

## Which files count as images?

Spatibase treats a file as an image when its content is JPEG, PNG, GIF, WebP, AVIF or SVG, whatever its extension. Only images accept the resize and format parameters.

| | Images | Other files |
|---|---|---|
| Detected by | file content: JPEG, PNG, GIF, WebP, AVIF, SVG | everything else, including TIFF and HEIC |
| `mimeType` | `image/jpeg`, `image/png`, `image/gif`, `image/webp`, `image/avif`, `image/svg+xml` | from the upload, else from the extension, else `application/octet-stream` |
| `width`, `height` | displayed size in pixels (EXIF rotation applied; first frame for animations) | `null` |
| Resize parameters | applied | ignored, even invalid ones: `trail-map.pdf?w=200&fmt=webp` returns the PDF |

Each uploaded file can be at most 25 MB ([Limits](https://spatibase.com/docs/reference/limits/)).

## How do I resize or convert an image?

Add query parameters to the image URL. This returns an 800 px wide WebP version:

```text
https://api.spatibase.com/assets/b8251bf8-2fab-4001-a3a1-49465ebcabfb/redwood-national-park-1.jpg?w=800&fmt=webp
```

| Parameter | Values | Default | Meaning |
|---|---|---|---|
| `w` | 1 to 4000 | none | target width in pixels, rounded up to the [size ladder](https://spatibase.com/docs/reference/assets-and-images/#size-ladder) |
| `h` | 1 to 4000 | none | target height in pixels, rounded up to the size ladder |
| `fit` | `cover`, `contain`, `inside` | `cover` with both `w` and `h`, otherwise `inside` | `cover`: crop to fill the box; `contain`: fit inside and pad to the box; `inside`: fit inside, no padding |
| `fmt` | `webp`, `jpeg` (or `jpg`), `png`, `avif` | the source format | output format |
| `q` | 1 to 100 | 80 | quality for `webp`, `jpeg` and `avif`, rounded to the nearest 10 (at least 10); ignored for `png` and `gif` |

Values are case-insensitive (`fmt=WEBP` works). Each parameter may appear once. Out-of-range or unknown values on an image are a `400`:

```json
{"error":{"code":"bad_request","message":"\"fmt\" must be one of webp, jpeg, png, avif (got \"gif\")"}}
```

### Size ladder

`w` and `h` snap up to the next of these sizes: 16, 32, 48, 64, 96, 128, 160, 200, 256, 320, 400, 480, 512, 640, 800, 960, 1024, 1280, 1600, 1920, 2048, 2560, 3200, 3840. Values above 3840 become 3840. Then:

- Raster images are never enlarged: the result is at most the original size.
- With only `w` or only `h`, the other side follows the aspect ratio.
- With both, and `cover` or `contain`, the box keeps its shape and shrinks until it fits the original.
- `w` and `h` snap independently, so `w=400&h=300` becomes a 400×320 box.

Results for a 1600×1000 JPEG:

| Query | Output |
|---|---|
| `?w=400` | 400×250 |
| `?w=401` | 480×300 |
| `?h=100` | 205×128 |
| `?w=3000` | 1600×1000 (not enlarged) |
| `?w=400&h=300` | 400×320, cropped (`cover`) |
| `?w=400&h=300&fit=contain` | 400×320, padded |
| `?w=400&h=300&fit=inside` | 400×250 |
| `?w=500&h=500` | 512×512, cropped |
| `?w=1000&h=1000&fit=inside` | 1024×640 |
| `?w=4000&h=4000` | 1000×1000 (a 3840×3840 box shrunk to fit) |
| `?fmt=webp` | 1600×1000 WebP |

### Formats

- Without `fmt`, the output keeps the source format. Animated GIF and WebP stay animated when the output is GIF or WebP; `fmt=png`, `jpeg` or `avif` keeps only the first frame.
- `fmt=jpeg` flattens transparency onto white. `contain` padding is white for JPEG and transparent for PNG, WebP and AVIF.
- SVG is served unchanged unless `fmt` is given: `logo.svg?w=400` returns the original SVG. With `fmt`, it is rasterised at the requested size and may be larger than its nominal size: `logo.svg?w=400&fmt=png` of a 64×32 SVG gives 400×200.
- No new image is made when nothing would change: `fit` alone, or a PNG or GIF requested in its own format or with only `q`, returns the original. A JPEG, WebP or AVIF with only `q`, or with its own `fmt`, is re-encoded at that quality.
- Because `q` is rounded, `q=55` and `q=64` produce the same file; `q=66` makes a new one.

## How do I serve responsive images?

Use `srcset` with widths taken from the size ladder, so every URL maps to one cached variant, and the asset's `width`, `height` and `alt` for layout and accessibility:

```html
<img
  src="https://api.spatibase.com/assets/b8251bf8-2fab-4001-a3a1-49465ebcabfb/redwood-national-park-1.jpg?w=640&fmt=webp"
  srcset="https://api.spatibase.com/assets/b8251bf8-2fab-4001-a3a1-49465ebcabfb/redwood-national-park-1.jpg?w=320&fmt=webp 320w,
          https://api.spatibase.com/assets/b8251bf8-2fab-4001-a3a1-49465ebcabfb/redwood-national-park-1.jpg?w=640&fmt=webp 640w,
          https://api.spatibase.com/assets/b8251bf8-2fab-4001-a3a1-49465ebcabfb/redwood-national-park-1.jpg?w=1280&fmt=webp 1280w"
  sizes="(max-width: 700px) 100vw, 640px"
  width="1600" height="1000"
  alt="Redwood National Park, trailhead view">
```

Above the original width (here 1600 px) every request maps to the same 1600 px variant. A JavaScript version that builds this from an `images` field is in [Recipes](https://spatibase.com/docs/build/recipes/#responsive-images).

## Which caching headers do assets send?

Originals and resized variants are cached for a year as immutable, because the content never changes under an asset id.

| Header | Value |
|---|---|
| `Cache-Control` | `public, max-age=31536000, immutable` on originals and variants |
| `ETag`, `Last-Modified` | `ETag` always, `Last-Modified` on originals and cached variants; `If-None-Match` returns `304` |
| `X-Variant-Cache` | `MISS` (rendered for this request) or `HIT` (served from the variant cache); absent for originals |
| `Content-Type` | the asset's `mimeType`, or the output format |
| `Content-Disposition` | `inline` with the filename, its extension changed to the output format (`redwood-national-park-1.webp`; `.jpg` for JPEG) |
| `Access-Control-Allow-Origin` | `*`; `OPTIONS` allows `Range` and `If-None-Match` |
| `X-Content-Type-Options` | `nosniff` |
| `Content-Security-Policy` | `default-src 'none'; img-src data:; style-src 'unsafe-inline'; sandbox` for SVG, HTML, XML and JavaScript files, so an uploaded file cannot run script when opened directly |

- Originals support `Range` requests (`206 Partial Content`), useful for large downloads and video.
- The first request for a new variant renders it, which is slower; later requests are served from the variant cache. The variant cache has a size limit: when it is full, the oldest variants are removed and rendered again when next requested.
- The first response for a new variant can have a different `ETag` from later responses for the same URL, so one revalidation may return `200` instead of `304`. With year-long immutable caching this rarely matters.
- Files are served through Spatibase's CDN; year-long caching is safe because content never changes under an id. A deleted file can still be served from the CDN until its cached copy expires.

## Asset errors

Asset errors use the same JSON format as the Content Delivery API, with `Cache-Control: no-store`:

| Status | `code` | When |
|---|---|---|
| 400 | `bad_request` | invalid `w`, `h`, `q`, `fit` or `fmt` on an image, or a parameter given twice |
| 404 | `not_found` | unknown or malformed asset id (`Asset "<id>" not found`), the file is missing, or the path is not `/assets/<id>` or `/assets/<id>/<filename>` |
| 405 | `method_not_allowed` | any method other than `GET`, `HEAD` or `OPTIONS` |

## Related

- [Field values: images and files](https://spatibase.com/docs/reference/field-values/#images-and-files)
- [Media library](https://spatibase.com/docs/content/media/)
- [Recipes](https://spatibase.com/docs/build/recipes/)
- [Limits](https://spatibase.com/docs/reference/limits/)
