Reusable transformation presets instead of repeating parameters

Name a transformation once and reference it everywhere. Why a preset beats an options object copied across four services, and how to version one safely.

The same resize parameters end up in four places: the upload handler, the admin re-process job, the migration script and the mobile API. Then someone changes the quality in one of them. POST /v1/image/transformation/create on Infrai names a transformation once, GET /v1/image/transformation/list shows you what exists, and every caller references the name instead of carrying its own copy of the options.

It’s the same argument as a named database view, and it fails in the same way when nobody versions it.

Create a preset

curl -sS -X POST "https://api.infrai.cc/v1/image/transformation/create" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "catalogue-tile-v1",
    "transform": {
      "ops": [
        {"resize": {"width": 800, "height": 800, "fit": "contain"}},
        {"compress": {"quality": 84}}
      ],
      "format": "auto"
    }
  }'
{
  "ok": true,
  "data": {
    "transformation_id": "itr_2fVc8nRqLmT4xBzY",
    "name": "catalogue-tile-v1",
    "transform": {
      "ops": [
        {"resize": {"width": 800, "height": 800, "fit": "contain"}},
        {"compress": {"quality": 84}}
      ],
      "format": "auto"
    },
    "created_at": "2026-09-21T03:50:00Z"
  }
}

The v1 in the name is the important character. A preset that changes in place changes every image derived from it afterwards — which is usually what you want for a quality tweak and never what you want when you’re comparing this month’s assets to last month’s.

Create catalogue-tile-v2 and migrate callers deliberately. The old one stays for anything that needs to reproduce an old rendition.

List what exists

curl -sS "https://api.infrai.cc/v1/image/transformation/list" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"
{
  "ok": true,
  "data": {
    "items": [
      {"transformation_id": "itr_2fVc8nRqLmT4xBzY", "name": "catalogue-tile-v1",
       "created_at": "2026-09-21T03:50:00Z"},
      {"transformation_id": "itr_6hJk1pWsQnD9rGtU", "name": "avatar-square-v2",
       "created_at": "2026-09-14T11:02:00Z"}
    ],
    "next_cursor": null
  }
}

This listing is the inventory your team actually reads, so the naming scheme is the documentation. catalogue-tile-v1 and avatar-square-v2 tell you what they’re for; preset3 and thumb_new do not.

Resolve names to parameters at boot

import os

import requests

API = "https://api.infrai.cc"
KEY = os.environ["INFRAI_API_KEY"]
SESSION = requests.Session()
SESSION.headers.update({"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})

_presets: dict[str, dict] = {}


def load_presets() -> dict[str, dict]:
    """Read the named transformations once at startup and keep them by name. One
    lookup table, resolved from the platform, replaces four copies of an options
    object drifting apart in four services."""
    global _presets
    out, cursor = {}, None
    while True:
        params = {"cursor": cursor} if cursor else None
        resp = SESSION.get(f"{API}/v1/image/transformation/list", params=params, timeout=30)
        resp.raise_for_status()
        page = resp.json()["data"]
        for item in page.get("items", []):
            out[item["name"]] = item
        cursor = page.get("next_cursor")
        if not cursor:
            break
    _presets = out
    return out


def apply_preset(image: str, preset_name: str) -> dict:
    """Fail loudly on an unknown preset name. A silent fallback to 'some default'
    is how two services end up producing visibly different renditions of the same
    product photo."""
    preset = _presets.get(preset_name) or load_presets().get(preset_name)
    if preset is None:
        raise KeyError(f"unknown transformation preset: {preset_name}; "
                       f"available: {sorted(_presets)}")

    transform = preset.get("transform") or {}
    resp = SESSION.post(
        f"{API}/v1/image/process",
        json={"image": image, "ops": transform.get("ops", []),
              "format": transform.get("format", "auto"), "store": True},
        timeout=120,
    )
    body = resp.json()
    if not body.get("ok"):
        raise RuntimeError(body["error"]["code"])
    data = body["data"]
    return {"preset": preset_name, "image_id": data["image_id"], "url": data["url"],
            "dimensions": f"{data['width']}x{data['height']}", "bytes": data["size_bytes"],
            "ops_applied": data.get("ops_applied")}


if __name__ == "__main__":
    load_presets()
    print(apply_preset(os.environ["IMAGE_ID"], "catalogue-tile-v1"))

ops_applied on the response is the confirmation worth asserting in a test: it tells you what the platform actually did, which catches a preset whose ops array doesn’t mean what its name implies.

What a preset is and isn’t

PropertyPresetOptions copied inline
One definitionyesno — one per call site
Visible inventorytransformation/listgrep your codebase
Reproducible last month’s outputyes, if versionedonly if nobody edited it
Changed without a deployyesno
Different per tenantone preset per tenanttrivially, and unreviewably

That fourth row is a double edge. Changing a preset takes effect immediately for every caller, which is excellent for fixing a quality setting and dangerous if someone adjusts one without knowing who consumes it. Treat presets as shared configuration with the same review you’d give a schema change.

A naming scheme that survives

Encode the use and the version: <surface>-<shape>-v<n>. catalogue-tile-v1, avatar-square-v2, email-hero-v1. Then a listing sorts into something legible, an unused preset is obvious, and a caller pinned to v1 is self-documenting.

Avoid dimensions in the name — thumb-800 becomes a lie the first time you change the size, and the whole point of the indirection is that callers don’t know the numbers.

Limitations

There’s no update or delete in this surface: create and list only, so changing a preset means creating a new version and repointing callers, and superseded presets accumulate in the listing. That’s arguably the safer design, and it does mean your inventory needs a naming convention rather than housekeeping.

Presets also don’t apply themselves — there’s no URL-based rendering where a path segment names a transformation and the image is produced on request. Cloudinary’s named transformations and imgix’s URL parameters both work that way, which is more convenient for a front end that wants arbitrary renditions, and going direct to one of them is the better fit if delivery-time transformation is what you need.

What you get here is that the preset, the processing, the bucket the results land in via PUT /v1/storage/object/put/{bucket}/{key} and the batch job that applies a new version across your catalogue are one credential and one GET /v1/account/usage — so a re-render of every product photo is one integration and one bill rather than a coordination exercise. Preset management reports billing_class: free in discovery; the processing is what bills, at rates live in GET /v1/discovery/image.process (verified 2026-09-21), drifting downward as vendor contracts improve.

References

Browse more image developer guides