# URL API (https://auraimage.ai/docs/url-api)



Every image is served from a deterministic, human-readable URL. Add a transform segment to the path to resize, crop, and re-format on the fly — the edge transforms once and caches forever.

## URL structure [#url-structure]

```
https://cdn.auraimage.ai/[projectName]/[transform]/[name][.ext]
```

| Segment       | Description                                                                             | Example           |
| ------------- | --------------------------------------------------------------------------------------- | ----------------- |
| `projectName` | Your project identifier — same as `NEXT_PUBLIC_AURA_PROJECT_NAME`.                      | `my-app`          |
| `transform`   | Optional comma-separated transform options — one path segment, right after the project. | `w=800,q=75`      |
| `name`        | The extension-less `name` returned by upload (or whatever you stored).                  | `abc123xyz0-hero` |
| `.ext`        | Optional format extension. Omit it for automatic format selection (recommended).        | `.avif`           |

```
https://cdn.auraimage.ai/my-app/w=800,q=75/abc123xyz0-hero
```

The transform segment is optional — `https://cdn.auraimage.ai/my-app/abc123xyz0-hero` serves the full-size image with automatic format negotiation.

## Transform options [#transform-options]

Transform options are a single comma-separated path segment placed immediately after the project name — `w=1920,fit=face,q=75`. Recognized keys are `w`, `h`, `fit`, and `q` (plus `lqip`, below). The grammar is **strict**: an unknown key, a duplicate key, or an invalid value returns `400` naming the offender — options are never silently ignored.

### Sizing [#sizing]

| Option | Type    | Default | Notes                                                                                                                     |
| ------ | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------- |
| `w`    | integer | —       | Requested width in pixels, snapped up to the dimension ladder below. Height scales proportionally unless `h` is also set. |
| `h`    | integer | —       | Requested height in pixels, snapped up to the dimension ladder below.                                                     |
| `fit`  | string  | `cover` | How to fit the image into the requested box. Only meaningful when `w` or `h` is set.                                      |

#### The dimension ladder [#the-dimension-ladder]

`w` and `h` are not honored verbatim. Each is rounded **up** to the next rung of a fixed ladder:

```
64  128  256  512  768  1024  1536  2048  3072  4096
```

So `w=400` delivers a 512px-wide image, `w=1200` delivers 1536px, and `w=1024` delivers exactly 1024px. Anything above `4096` clamps to `4096`.

This is deliberate: it collapses the near-infinite space of requested widths onto ten variants, so the edge cache stays warm instead of fragmenting across every value a layout happens to ask for. The practical consequence is that you get an image at least as large as you asked for — never smaller — and that requesting a rung exactly is the only way to avoid paying for extra pixels. Size your layouts to the ladder and the bytes on the wire match the bytes on screen.

**`w` and `h` snap independently, which can change your aspect ratio.** Send `w` alone and the height is derived from the source, so the ratio is preserved: `w=800` on a 1536×1024 original delivers `1024×683`. Send both and each is rounded up on its own — `w=800,h=533` asks for 3:2 but delivers `1024×768`, a 4:3 box that `fit=cover` then crops to fill.

So pass `h` only when you actually want a fixed box, and when you do, put both values on rungs that already form the ratio you want — `1536×1024` and `768×512` are both exactly 3:2. Otherwise send `w` by itself and let the source aspect carry through.

**`fit` values:**

| Value     | Behavior                                                                           |
| --------- | ---------------------------------------------------------------------------------- |
| `cover`   | Crop to fill the exact box.                                                        |
| `contain` | Letterbox — full image visible, no crop.                                           |
| `face`    | Cloudflare-native face detection. Crops the most prominent face into the box.      |
| `auto`    | Cloudflare-native saliency detection. Crops to the most visually important region. |

When `fit=face` or `fit=auto` succeeds, the response carries `X-Aura-Smart-Crop: ok`. When no face / salient region is detected, the result falls back to a centered `cover` crop (the header is omitted).

### Quality [#quality]

| Option | Type  | Default | Notes                                                                  |
| ------ | ----- | ------- | ---------------------------------------------------------------------- |
| `q`    | 1–100 | `80`    | Compression quality. Ignored for lossless masters served pass-through. |

### Format [#format]

Output format is chosen by the URL's **trailing extension** — there is no `fmt` option.

| Extension        | Behavior                                                                                                                         |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| *(none)*         | **Recommended.** Automatic — picks the best format the client accepts (AVIF → WebP → JPEG). The extension-less URL is canonical. |
| `.avif`          | Force AVIF.                                                                                                                      |
| `.webp`          | Force WebP.                                                                                                                      |
| `.jpg` / `.jpeg` | Force JPEG.                                                                                                                      |
| `.png`           | Force PNG.                                                                                                                       |

Because the extension-less URL negotiates the best modern format automatically, prefer it for `<img>` tags. Pin an explicit extension only when the consumer can't send an `Accept` header — most importantly `og:image`, where you should use an explicit `.jpg` or `.png`.

Requesting a known-but-non-servable extension (`.gif`, `.heic`, `.tiff`, `.bmp`) returns `400` with guidance: request `/{name}` for automatic format, or one of `.jpg`, `.png`, `.webp`, `.avif`.

### Loading helpers [#loading-helpers]

| Option | Type | Description                                                                                                                                       |
| ------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lqip` | flag | `lqip=true` returns a low-quality image preview (small, heavily compressed) for placeholder rendering — e.g. `/my-app/lqip=true/abc123xyz0-hero`. |

For a BlurHash placeholder, fetch it as JSON from `GET /v1/blurhash/<projectName>/<name>` — `{ "blurhash": "..." }`. The `<AuraImage />` component uses this internally.

## Build URLs from code [#build-urls-from-code]

Rather than concatenating the path yourself, the [`@auraimage/sdk`](https://www.npmjs.com/package/@auraimage/sdk) package ships two helpers that serialize these URLs for you — handy when you set an image `src` manually instead of using the [`<AuraImage />`](/docs/image) component. Both are pure and **browser-safe** (they take no secret key and make no network calls), and both build **public** URLs — for a private image, sign one with [`getSignedUrl`](/docs/private-images) instead.

`buildServeUrl` takes friendly parameter names and emits the transform segment, format extension, and cache-buster in the correct grammar:

```ts
import { buildServeUrl } from '@auraimage/sdk';

buildServeUrl({
  cdnUrl: 'https://cdn.auraimage.ai',
  project: 'my-app',
  name: 'blog/hero', // the extension-less name returned by upload
  width: 800,
  height: 600,
  quality: 75,
  format: 'auto', // 'auto' | 'jpeg' | 'png' | 'webp' | 'avif'
  v: 3 // optional cache-buster
});
// → https://cdn.auraimage.ai/my-app/w=800,h=600,q=75/blog/hero?v=3
```

Only `cdnUrl`, `project`, and `name` are required. The rest map to the grammar above — `width→w`, `height→h`, `fit→fit`, `quality→q`, `lqip→lqip=true`, and `format` to the trailing extension — and any option you omit is left out of the URL. `width`/`height` (positive integers) and `quality` (1–100) are validated: a bad value throws instead of producing a URL the CDN would reject.

`buildBlurhashUrl` builds the placeholder-metadata URL from [Loading helpers](#loading-helpers):

```ts
import { buildBlurhashUrl } from '@auraimage/sdk';

const url = buildBlurhashUrl({ cdnUrl: 'https://cdn.auraimage.ai', project: 'my-app', name: 'blog/hero' });
// → https://cdn.auraimage.ai/v1/blurhash/my-app/blog/hero

const { blurhash, width, height } = await fetch(url).then((r) => r.json());
```

## Content negotiation [#content-negotiation]

When the URL has no extension (automatic format), the edge reads the `Accept` header and serves in priority order:

```
AVIF  →  WebP  →  JPEG
```

The response carries `Vary: Accept` so the cache stores one variant per accept-class — no cross-browser cache poisoning.

## Query parameters [#query-parameters]

Only two query parameters are honored:

| Param   | Purpose                                                            |
| ------- | ------------------------------------------------------------------ |
| `v`     | Cache-buster. Bump it to force a fresh variant after an overwrite. |
| `token` | Serve token for [private images](/docs/private-images).            |

Transform options **must** live in the path segment. A legacy transform query param (`?w=800`, `?fmt=auto`, `?blur=true`, …) returns `400` — a tripwire so a half-migrated URL fails loudly instead of silently serving a full-size original. All other query params (`utm_*`, `fbclid`, …) are ignored, so link decoration never breaks an image or fragments the cache.

## SEO names [#seo-names]

Descriptive names improve image rankings in Google Images. An image's `name` is its extension-less identity — set a meaningful one at upload time (via the upload token's `name` claim, or let it be auto-generated from a descriptive uploaded file name):

```
# Good — descriptive
https://cdn.auraimage.ai/my-app/abc123xyz0-golden-gate-bridge-sunset

# Avoid — opaque
https://cdn.auraimage.ai/my-app/abc123xyz0-img-00432
```

Use hyphens as separators. Names may contain `/` to group images into path segments (e.g. `blog/hero`).

## Project names are immutable [#project-names-are-immutable]

A project name is permanent. Renaming would break every published URL, so the dashboard does not allow it. Pick carefully during `aura init` or MCP install.

**Reserved names** (cannot be used as a project name): `api`, `admin`, `cdn`, `health`, `registry`, `static`, `test`, `v1`.

## Caching behavior [#caching-behavior]

| Visibility          | `Cache-Control`                       | Edge cache                                                              |
| ------------------- | ------------------------------------- | ----------------------------------------------------------------------- |
| Public              | `public, max-age=31536000, immutable` | Yes — first request transforms, every subsequent request hits the edge. |
| Private (`?token=`) | `private, max-age=300`                | No — every request bypasses the shared edge cache and hits origin.      |

**Cache tags** are set on every public response so you can purge surgically:

| Tag                     | Applies to                            |
| ----------------------- | ------------------------------------- |
| `project-<projectName>` | Every public image in the project.    |
| `fit-face`              | Public images served with `fit=face`. |
| `fit-auto`              | Public images served with `fit=auto`. |

Purge a single project from the dashboard without affecting any other project, and re-render smart-cropped variants without invalidating exact-size crops.

## Response headers [#response-headers]

Every successful image response carries:

| Header              | Values          | Meaning                                                                    |
| ------------------- | --------------- | -------------------------------------------------------------------------- |
| `X-Aura-Cache`      | `HIT` \| `MISS` | Whether the edge cache served this response.                               |
| `X-Aura-Smart-Crop` | `ok`            | Set only when `fit=face` / `fit=auto` produced a crop. Absent on fallback. |
| `Server-Timing`     | see below       | Per-stage timings, set on **both** hits and misses.                        |
| `Vary`              | `Accept`        | Required for correct format negotiation caching.                           |

### Server-Timing [#server-timing]

A standard `Server-Timing` header lets you measure delivery in DevTools or RUM without any custom instrumentation.

**On a cache hit:**

```
Server-Timing: cache;desc="HIT";dur=<ms>
```

**On a cache miss:**

```
Server-Timing: cache;desc="MISS";dur=<ms>, r2;dur=<ms>, process;desc="transformed";dur=<ms>
```

Pass-through misses (no transformation needed) report `process;desc="passthrough";dur=0`.

| Stage     | What it measures                                                                                       |
| --------- | ------------------------------------------------------------------------------------------------------ |
| `cache`   | Edge cache lookup.                                                                                     |
| `r2`      | Origin object fetch from R2.                                                                           |
| `process` | Transformation (resize, format conversion, smart crop). `passthrough` when the master is served as-is. |

Open Chrome DevTools → Network → click any image response → Timing tab to see these stages plotted alongside browser timings.


## Related

- [Image](https://auraimage.ai/docs/image)
- [Private Images](https://auraimage.ai/docs/private-images)
- [Speed Demo](https://auraimage.ai/speed)