> ## Documentation Index
> Fetch the complete documentation index at: https://docs.openinary.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Transform Media

> Resize, crop, convert and trim media by putting parameters in the URL

Put comma-separated parameters between `/t/` and the file path. The first request renders and caches the result, the rest are served from cache.

```
GET /t/{transformations}/{path}
```

```http theme={null}
GET /t/w_800,h_600,c_fill,f_webp/photos/portrait.jpg
```

Drop the parameters to get the stored original back untouched: `GET /t/photos/portrait.jpg`.

**Auth:** public, rate limited. For URLs that cannot be guessed or edited, use [signed URLs](https://docs.openinary.dev/api-reference/media/authenticated-transform.md), which are self-hosted only.

On Cloud the same path sits behind a bucket prefix, `/b/{bucketId}/t/...`. See [your base URL](https://docs.openinary.dev/media-transformations/overview.md#your-base-url). Everything after it is identical.

## Recipes

| Goal | URL |
| - | - |
| Resize | `/t/w_800,h_600/image.jpg` |
| Avatar, cropped on the subject | `/t/w_400,h_400,c_fill,g_face/portrait.jpg` |
| Circular avatar | `/t/w_400,h_400,c_fill,r_max,f_png/avatar.jpg` |
| Rounded corners on a white page | `/t/w_600,h_400,r_16,b_rgb:ffffff/photo.jpg` |
| 16:9 banner | `/t/ar_16:9,w_1920/banner.jpg` |
| Padded to a fixed size | `/t/w_400,h_300,c_pad,b_rgb:f5f5f5/thumb.jpg` |
| Padded with a color taken from the image | `/t/w_400,h_300,c_pad,b_auto/product.jpg` |
| Brighten a dark photo | `/t/w_800,e_fill_light:70:20/photo.jpg` |
| Blurred placeholder | `/t/w_40,e_blur:400/photo.jpg` |
| Black and white | `/t/w_800,e_grayscale/photo.jpg` |
| Smallest file the browser supports | `/t/w_1200/photo.jpg` |
| 720p video | `/t/w_1280,h_720,q_80/video.mp4` |
| Trim 5s to 12s | `/t/so_5,eo_12/video.mp4` |
| Video poster frame | `/t/t_true,tt_5,f_webp,w_500/video.mp4` |

## Image parameters

| Param | Values | Default | Notes |
| - | - | - | - |
| `w` | integer | | Width in pixels |
| `h` | integer | | Height in pixels |
| `c` | `fill` `fit` `scale` `crop` `pad` `lfill` `lpad` `mfit` `mpad` | `fill` | `fill` crops the overflow, `fit` preserves ratio without enlarging, `scale` may stretch, `pad` adds borders in `b`. See [limited modes](#limited-crop-modes) |
| `ar` | `W:H` | | Aspect ratio, e.g. `ar_16:9` |
| `a` | degrees, `auto`, `ignore`, `hflip`, `vflip`, joined with `.` | | Rotate and mirror, left to right: `a_hflip.90` mirrors then rotates. EXIF orientation is always applied first (`auto` adds nothing); `ignore` keeps the stored pixel orientation |
| `dpr` | `0.1` to `5`, or `auto` | `1` | Multiplies `w` and `h`: `dpr_2,w_400` is `w_800`. `auto` serves 1x, Client Hints are not read |
| `g` | `center` `north` `south` `east` `west` `face` `auto` | `center` | Focal point when cropping |
| `b` | `rgb:<hex>` (3, 4, 6 or 8 digits), a CSS color name, `transparent`, [`auto`](#automatic-background), or `blurred[:intensity][:brightness]` | | Fill color for `c_pad`, rotation, rounded corners and `o`. Alias `bg` |
| `r` | integer, `v1:v2`, `v1:v2:v3`, `v1:v2:v3:v4`, or `max` | | Corner radius, clockwise from top-left. `max` gives a circle or oval |
| `q` | `1` to `100`, or `auto` | `auto` | See [quality](#quality) |
| `f` | `avif` `webp` `jpeg` `png` `gif` `auto` | browser's best | See [formats](#formats) |
| `e` | see [effects](#effects) | | Repeatable, applied in URL order |
| `o` | `0` to `100` | `100` | Opacity. Baked onto `b` (or white, for `f_jpg`) when set, otherwise kept as transparency |
| `co` | `rgb:<hex>` or a CSS color name | `gray` | Color for `e_colorize` |
| `bo` | `<width>px_solid_<color>` | | Border outside the image, 1 to 1000 px, e.g. `bo_4px_solid_rgb:333` |
| `d` | file path, `:` for folders | | Image served, with the same transformation, when the requested one is missing. See [default image](#default-image) |

`g_face` uses saliency detection, not face detection. It keeps whatever region scores as most interesting, which on a portrait is normally the face.

Corner radius is clamped to half the shorter side. Cut corners are transparent, so pair `r` with `f_png`, `f_webp` or `f_avif`, or add `b` to fill them and stay on JPEG.

### Limited crop modes

| Mode | Larger than the box | Smaller than the box |
| - | - | - |
| `lfill` | Like `fill` | Not enlarged, only cropped to the box |
| `lpad` | Like `pad` | Not enlarged, padded to the exact size |
| `mfit` | Untouched | Enlarged to fit, like `fit` without its limit |
| `mpad` | Untouched | Not enlarged, padded to the exact size |

`c_limit` is the same as `c_fit`, which never enlarges. `lpad` and `mpad` place the image by `g` (`north`, `south`, `east`, `west`, default `center`) and fill with `b`.

### Automatic background

`b_auto` picks the fill color from the image, so a padded product shot does not end up with a jarring white band. Use it with `c_pad`.

```
b_auto[:mode][:number][:direction][:palette_<color1>_..._<colorN>]
```

| Mode | Color taken from |
| - | - |
| `border` (default) | The predominant color of the image's edges |
| `predominant` | The predominant color of the whole image |
| `border_contrast`, `predominant_contrast` | The color that contrasts most with the above |
| `border_gradient`, `predominant_gradient` | A gradient between the predominant colors of each side |
| `border_gradient_contrast`, `predominant_gradient_contrast` | A gradient of the contrasting colors |

For gradients, `number` is `2` (default) or `4`. With 2 colors, `direction` is `horizontal` (default), `vertical`, `diagonal_desc` or `diagonal_asc`. With 4, each corner gets its own color. `palette_` limits the result to the listed colors, as CSS names or hex without `rgb:`, for example `b_auto:predominant:palette_white_black_navy`.

`b_blurred` pads with a blurred copy of the image itself, the usual way to fit a portrait into a landscape slot: `c_pad,w_1200,h_630,b_blurred:400:-20`. Intensity runs `1` to `2000` (default `100`), brightness `-300` to `100` (default `0`).

### Default image

`d_placeholders:avatar.png` serves `placeholders/avatar.png` when the requested file does not exist, with the rest of the transformation applied, so `w_96,h_96,c_fill,d_avatar.png/users/42.jpg` always returns a 96 px image. The response carries `X-Default-Image` and `Cache-Control: private, max-age=60`, so no shared cache pins the placeholder once the real file is uploaded. If the default image is missing too, the answer is `404`.

> [!NOTE]
>
> Self-hosted only for now. On Cloud `d_` is accepted and ignored: a missing file still answers `404`.

### Effects

Effects run after resizing, so they work on the delivered pixels, and before the overlay, so a watermark is left untouched. Put several in one segment and they apply left to right: `e_negate,e_brightness:30` and `e_brightness:30,e_negate` give different results.

| Effect | Arguments | Default | Notes |
| - | - | - | - |
| `e_fill_light[:blend][:bias]` | blend `0` to `100`, bias `-100` to `100` | `100`, `0` | Lifts shadows and midtones, leaves highlights. An approximation: Cloudinary does not publish its algorithm |
| `e_blur[:strength]` | `1` to `2000` | `100` | Gaussian blur |
| `e_sharpen[:strength]` | `1` to `2000` | `100` | |
| `e_unsharp_mask[:strength]` | `1` to `2000` | `100` | Wider radius than `e_sharpen` |
| `e_grayscale` | | | Keeps transparency |
| `e_sepia[:level]` | `1` to `100` | `80` | |
| `e_negate` | | | |
| `e_brightness[:level]` | `-99` to `100` | `80` | Percent change, `100` doubles |
| `e_contrast[:level_<n>][;type_<t>]` | `sigmoidal`: `-100` to `100`; `linear`: `1` to `200` | `0`, `sigmoidal` | `e_contrast:50` also works |
| `e_saturation[:level]` | `-100` to `100` | `80` | `-100` is grayscale |
| `e_hue[:level]` | `-100` to `100` | `80` | `100` is half a turn of the color wheel |
| `e_gamma[:level]` | `-50` to `150` | `0` | Higher brightens midtones |
| `e_pixelate[:size]` | `1` to `200` px | from the image size | |
| `e_trim[:tolerance][:color]` | `0` to `100`, color | `10`, corner pixel | Crops away a uniform border |
| `e_tint[:equalize][:amount][:color[:Np]]...` | amount `0` to `100`, up to 10 colors with positions `0p` to `100p` | `60`, `red` | Several colors map dark to light tones |
| `e_colorize[:level]` | `0` to `100` | `100` | Color from `co`, e.g. `co_darkviolet,e_colorize:35` |

Colors take `rgb:<hex>` with 3, 4, 6 or 8 digits, or a CSS color name. `e_tint` takes the hex without `rgb:`, as Cloudinary does.

### Quality

Leave `q` unset and each format gets the quality it deserves: 80 for AVIF, 85 for WebP, 90 for JPEG, 100 for PNG. Large files shift a few points, clamped to 70-95. An explicit `1` to `100` is used as given.

### Formats

Omit `f` and Openinary picks the best format the browser accepts, in order AVIF, WebP, then JPEG or PNG. `f_auto` goes further: it encodes every supported candidate, compares byte counts and serves the smallest, at the cost of a slower first request.

**Input:** JPEG, PNG, WebP, AVIF, GIF (first frame), HEIC/HEIF (converted to JPEG at upload), PSD (layers composited)

**Output:** AVIF, WebP, JPEG, PNG, GIF

## Video parameters

| Param | Values | Default | Notes |
| - | - | - | - |
| `w` | integer | | Width in pixels |
| `h` | integer | | Height in pixels |
| `c` | `fill` `crop` `fit` `scale` `pad` | `fill` | Requires `w` or `h` |
| `q` | `1` to `100`, or `auto` | `60` | CRF is `round(51 - q / 100 * 33)`, so `q_60` is CRF 31 |
| `so` | seconds | | Start offset |
| `eo` | seconds | | End offset, exclusive. `so_5,eo_12` is a 7s clip |
| `f` | `mp4` `webm` `mov`, or an image format | `mp4` | An image format extracts a frame |
| `t` | `t_true` | `false` | Extract one frame as an image instead of transcoding |
| `tt` | seconds | `0` | Which frame to extract, with `t` |

A bare `/t/clip.mp4` streams the original untouched. Add `q_auto` to opt into the default encode without choosing a CRF.

The default `q_60` favors encoding speed over file size, which is what makes large sources viable. Raise it when output quality matters more than job time.

Videos also apply fixed limits: 720p maximum when you request no explicit `w` or `h`, a 200 MB cap on the source, and a 5 minute encode timeout. See [video processing](https://docs.openinary.dev/configuration/video-processing.md).

**Input:** MP4 (H.264/H.265), MOV, WebM (VP8/VP9) · **Output:** MP4, WebM, MOV

> [!WARNING]
>
> A video transformation that is not cached yet answers `202`, not the
> original. Wait for `Retry-After` and request the same URL again, see
> [Waiting on a 202](#waiting-on-a-202). For assets you know you will serve,
> [prewarm at upload](https://docs.openinary.dev/media-transformations/upload-and-prewarm.md) so the first
> visitor never sees the `202`.

## Pass-through formats

Audio and 3D files are stored and delivered, never transformed. Request them with no transformation segment and the stored bytes come back untouched, as in `GET /t/audio/podcast.mp3` or `GET /t/models/duck.glb`.

| Kind | Extensions |
| - | - |
| Audio | `wav` `mp3` `ogg` |
| 3D | `glb` `gltf` |

Adding parameters to one returns `400` rather than the original, so a "resized" variant that is really the untouched file never gets cached.

## Response

Returns the binary content, with `Cache-Control` set for browser caching and `X-Content-Type-Options: nosniff`. The content type comes from the extension via the shared media table, never from sniffing the body, which is why uploads are validated by content in the first place.

| Code | Meaning |
| - | - |
| `200` | File returned |
| `202` | Transformation still processing. Carries an estimate of when it will be ready, see below |
| `400` | Transformation parameters on a non-transformable type |
| `404` | File not found |
| `500` | Processing error |

## Waiting on a 202

A `202` means the variant is being generated and nothing is wrong. It always says when to come back:

```http theme={null}
HTTP/1.1 202 Accepted
Retry-After: 6
X-Processing-Phase: processing
X-Processing-Eta: 6
```

```json theme={null}
{
  "status": "processing",
  "message": "Video transformation is being processed",
  "phase": "processing",
  "progress": 0.42,
  "estimatedSeconds": 6,
  "estimatedRange": [6, 9],
  "retryAfter": 6,
  "statusUrl": "/video-status/w_640/videos/clip.mp4"
}
```

| Field | Type | Notes |
| - | - | - |
| `phase` | string | `waking`: the processing server is starting up (Cloud only, after it has been idle). `queued`: waiting behind other videos. `processing`: being encoded |
| `progress` | number | From `0` to `1`. Measured from the encoder for videos, `0` until it reports |
| `estimatedSeconds` | number | Typical time left, in seconds |
| `estimatedRange` | `[number, number]` | Typical and pessimistic time left. Show the second as an upper bound |
| `retryAfter` | number | Same as the `Retry-After` header: when asking again is worth it. Capped at 30 seconds, so a long job keeps refining its estimate |
| `statusUrl` | string | Videos only. See [video status](https://docs.openinary.dev/api-reference/video/status.md) |

The simplest client honours `Retry-After` and requests the **same URL** again until it gets a `200`. Every retry returns a fresh estimate, sharper as the job advances: once a video encode has started, the estimate comes from its own measured speed. Retrying sooner than `Retry-After` is harmless but cannot make the transformation finish faster.

```ts theme={null}
async function fetchVariant(url: string): Promise<Response> {
  for (;;) {
    const response = await fetch(url);
    if (response.status !== 202) return response;
    const seconds = Number(response.headers.get("Retry-After")) || 2;
    await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
  }
}
```

Estimates are learned from the transformations your deployment has already run, so they get more accurate with use. Before that they fall back to conservative defaults.

## Gotchas

Only the short parameter keys work. The parser splits a segment at its first underscore, so `thumbnail_time_5` reads as the key `thumbnail`, fails, and the segment is then treated as a folder name, ending in a `404`.

Video URLs accept the image-only parameters `a`, `ar`, `r`, `b`, `e`, `o`, `co` and `bo` and silently ignore them. The URL still works, the output is just re-encoded without them.

For compatibility with Cloudinary-shaped URLs, `c` also accepts `fill_pad` (as `fill`), `limit` (as `fit`) and `thumb` (as `crop`), and `g` also accepts `c`, `n`, `s`, `e`, `w`, `faces`, `face_center`, `faces_center`, `north_center` and `south_center`.

## Related

> [card] [Authenticated Transform](https://docs.openinary.dev/api-reference/media/authenticated-transform.md)
>
> Signed URLs that cannot be guessed or edited.

> [card] [Upload Files](https://docs.openinary.dev/api-reference/files/upload.md)
>
> Upload media and pre-generate variants.

> [card] [Video Status](https://docs.openinary.dev/api-reference/video/status.md)
>
> Poll a queued video transformation.

> [card] [Automatic Format Selection](https://docs.openinary.dev/media-transformations/intelligent-compression.md)
>
> How the default format and quality are chosen.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.