URL API
Deterministic, human-readable URLs for transformed image delivery.
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
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-heroThe 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 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
| 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
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 4096So 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
| Option | Type | Default | Notes |
|---|---|---|---|
q | 1–100 | 80 | Compression quality. Ignored for lossless masters served pass-through. |
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
| 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
Rather than concatenating the path yourself, the @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 /> 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 instead.
buildServeUrl takes friendly parameter names and emits the transform segment, format extension, and cache-buster in the correct grammar:
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=3Only 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:
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
When the URL has no extension (automatic format), the edge reads the Accept header and serves in priority order:
AVIF → WebP → JPEGThe response carries Vary: Accept so the cache stores one variant per accept-class — no cross-browser cache poisoning.
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. |
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
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-00432Use hyphens as separators. Names may contain / to group images into path segments (e.g. blog/hero).
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
| 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
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
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.