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
| Property | Preset | Options copied inline |
|---|---|---|
| One definition | yes | no — one per call site |
| Visible inventory | transformation/list | grep your codebase |
| Reproducible last month’s output | yes, if versioned | only if nobody edited it |
| Changed without a deploy | yes | no |
| Different per tenant | one preset per tenant | trivially, 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.