# Image URLs

URL: /docs/plugins/payload-images/image-urls

Read an image doc and get ready-made src and srcset — plus the full anatomy and every transform parameter behind the /api/img URL.

Every optimized image on your site is one URL against one endpoint, `/api/img` — public by default,
and closable with `options.access.serve`.

You rarely write these URLs by hand. Every read of an image doc hands you a ready-made `src` and `srcset`, so the fastest integration is: read the doc, use the URL. Reading a single URL is also the fastest way to understand the whole pipeline.

## The virtual URL fields

You don't need JavaScript to consume these URLs. Every read of an image doc — REST, GraphQL, Local API, and populated relationships — carries ready-made URLs as **virtual fields**, computed on read and never stored:

```jsonc
// GET /api/images/<id>  (or a populated page.heroImage)
{
  "id": "...", "alt": "...", "width": 2400, "height": 1600,
  "src": "https://site.com/api/img/<id>?w=1280&fit=cover&q=90&fmt=auto&v=…",
  "srcset": "https://site.com/api/img/<id>?w=400&h=267&fit=cover&q=90&fmt=auto&v=… 400w, …",
  "placeholderURL": "https://site.com/api/img/<id>?w=32&h=21&fit=cover&q=40&fmt=auto&v=…",
  "thumbnailURL": "https://site.com/api/img/<id>?w=160&h=160&fit=cover…"
}
```

That's the whole integration story for a mobile app, a GraphQL client, or an RSS/OG generator: read the doc, use the URL. The URLs are absolute when `serverURL` is set, relative otherwise. They flow through relationship population, so a populated `page.heroImage` is render-ready with no second query.

By default a read gets natural-ratio URLs. Declare the render in the read's `context` to get `src`/`srcset` built for exactly that render:

```ts title="Declare the render on the read"
// findByID / find / GraphQL — pass a render declaration in context
context: { image: { aspectRatio, quality, fit, format } }
```

The `srcset` follows the project-wide `pixelStep` (default `50`): one URL every 50px up to the source width (capped at `maxDimension`, 4096), with the top rung being the source's own width. An array `pixelStep` switches to a breakpoint ladder instead (e.g. next/image's `[640, 750, 828, 1080, 1200, 1920, 2048, 3840]`) — rungs below the source width plus the source width itself as the top rung; freeform requests still snap to the 50px grid.

> The dense default means a wide original emits dozens of `srcset` URLs — several KB of string on
> every read and population. Pass an array `pixelStep` (a fixed breakpoint ladder) if response size
> matters more than fine-grained width selection.

> The resolved config (slugs + options) is stashed on `config.custom.payloadImages`, so decoupled
> tooling (an OG/sitemap generator, a CDN-purge script, a migration) can read it from just
> `payload`, with no import.

## URL anatomy

When you do build a URL by hand, here's the shape:

```
/api/img/64a1f2c…?w=1280&h=720&fit=cover&q=80&fmt=auto&v=1a2b3c
         └ id     └ size ─────┘ └ crop ─┘ └ encode ──┘ └ cache ┘
```

- **`/api/img/:id`** — the transform endpoint, mounted under Payload's API route. The id is a doc
  in your source collection (`images` by default).
- **`w=1280&h=720`** — the output box. Providing both pins the geometry; the endpoint focal-crops
  into it.
- **`fit=cover`** — how the source maps into the box. `cover` crops around the
  [focal point](/docs/plugins/payload-images/metadata#focal-point-hotspot--crop).
- **`q=80&fmt=auto`** — encode settings. `auto` negotiates AVIF/WebP from the browser's `Accept`.
- **`v=1a2b3c`** — a cache-bust token derived from the source's filename, filesize, and focal
  layers (focal point, plus hotspot/crop when set). The server
  ignores it; it exists so the URL changes when the image does
  ([Caching](/docs/plugins/payload-images/caching)).

The first request for a URL generates the variant with Sharp and streams it same-origin. The variant persists after the response. Every later identical request streams the stored copy.

```
GET /api/img/<id>?w=600&h=600&fit=cover           # focal cover-crop, auto format
GET /api/img/<id>?w=1200&h=630&fit=cover&fmt=avif # an OG-sized AVIF
```

## Parameters

| Param     | Default | Meaning                                                                                              |
| --------- | ------- | ---------------------------------------------------------------------------------------------------- |
| `w` / `h` | —       | Target width / height in px. At least one is required.                                               |
| `ar`      | —       | Aspect ratio (`16:9` or `1.78` — `:` is the only accepted separator). Derives the missing dimension. |
| `fit`     | `cover` | How the source maps into the box: `cover` (focal crop) · `contain` · `inside` · `outside` · `fill`.  |
| `q`       | `90`    | Quality, bucketed to multiples of 5 and clamped to `qualityRange`.                                   |
| `fmt`     | `auto`  | Encode format: `auto` (negotiate from `Accept`) · `avif` · `webp` · `jpeg` · `png`.                  |
| `v`       | —       | Cache-busting token (derived from filename + filesize + focal layers). Ignored by the server.        |

`w` and `h` snap to the `pixelStep` grid (default 50px) and clamp to `maxDimension` (default `4096`). An array `pixelStep`'s exact widths also pass the snap. `q` clamps to `qualityRange` (default `40–95`). Output never upscales past the source.

Snapping and bucketing aren't cosmetic. They collapse the continuous parameter space into a finite set of possible variants. That's what makes the endpoint safe to expose ([the DoS bounds](/docs/plugins/payload-images/caching#caching--abuse-limits)) and the cache effective. See [Reference](/docs/plugins/payload-images/reference) for these options and their defaults.

> **The endpoint is public and unsigned by default.** Generation is gated by the bounds above rather
> than a shared secret. For fully untrusted traffic, raise `pixelStep` or lower `maxDimension` /
> `maxInputPixels` and put a rate limiter or CDN in front —
> [Reference](/docs/plugins/payload-images/reference) has every knob. To gate the endpoint itself,
> set `options.access.serve`; the source collection's read access is enforced either way.

## Format negotiation

With `fmt=auto` the endpoint picks the best format the browser's `Accept` header allows. WebP is the default modern target. AVIF only wins when `transform.preferAvif` is on, because AVIF encodes far slower and `auto` keeps the cold path fast. Explicit `fmt=avif` always works.

`fmt=auto` responses carry `Vary: Accept` so caches keep per-format copies straight. Explicit-format responses don't need it.
