---
title: "Transform an image synchronously"
method: POST
path: "/api/v1/images/transform"
tags: ["Synchronous images"]
---

# Transform an image synchronously

`POST /api/v1/images/transform`

## Request

- Base URL: `https://api.imagestep.dev`
- URL: `https://api.imagestep.dev/api/v1/images/transform`
- Auth: API key in header `Authorization`

Runs ONE deterministic op — or ONE saved preset with no AI step — on ONE image and
answers with the result bytes. Nothing is stored: no job, no asset, no public URL.

The image is the request body; the op and its parameters are query parameters:
`?op=resize&width=1200`. An op takes only what its catalogue entry declares, so
resizing AND re-encoding is two steps, and two steps are a preset:
`?preset=<slug>` (or `<slug>@<version>`) runs all of its steps in this one call, and
takes no parameters beside it. Which ops may be used here is `GET /api/v1/ops` →
`syncEndpoint`; do not hard-code a list.

THREE WAYS TO SEND THE IMAGE: the raw bytes with their own `Content-Type` (the format is
read from that header, so `application/octet-stream` on a camera RAW is `unsupported_format`);
`multipart/form-data` with a `file` part; or `application/json` with a reference instead of
bytes — `{assetId}` (one of your assets) or `{url}` (fetched by this service: `http(s)` only,
no private addresses, no redirects) — and then `op` / `preset` / `parameters` may ride in the
body too; the query string wins where both say something.

Which presets may be used here follows from their steps: only deterministic steps,
however many, run here. A preset with an AI step, or with a step that reads a stored
layer (`composite`, `overlay`) — or any other step that names one of your assets as
its second image (`boolean`, `joinChannel`) — is `400 invalid_param` on `preset`: it is
still a valid preset, and the same reference runs as a job.

LIMITS: a body of at most 25 MB, an input of at most 50 megapixels (a bigger one is `400 unsupported_format`: it is refused on the size its header declares, before it is decoded), 10 s of wall clock (`503`, `deadline_exceeded`). At most 4 calls in flight per account: one more is `429 rate_limited` with `details.reason = "account_concurrency"` and `Retry-After: 1`. A success counts one against the plan's processing allowance, however many steps a preset holds; past it, a success is paid from your balance at the plan's `overageCredits` (`GET /api/v1/ops`), and a call the balance cannot cover is `402 insufficient_credit` before anything runs. A failure counts and costs nothing. No `Idempotency-Key`: nothing is created, so send it again.

USE THIS WHEN:
- You are holding an image and only want the result back
- You do not want the input or the output stored

DO NOT USE WHEN:
- The op or a preset step is an AI op, or you have more than one image → POST /api/v1/jobs
- You want an asset id or a permanent URL → POST /api/v1/jobs

## Query parameters

- `op` string
- `preset` string
- `response` 'url'

## Request body

- SyncReferenceInput: The JSON form of a synchronous call: a reference to the image instead of its bytes. Give `assetId` or `url`. On `/images/transform` the op, preset and parameters may ride here too; a query parameter of the same name wins.
  - `op` string: Transform only: the op, as `?op=`
  - `preset` string: Transform only: the preset, as `?preset=`
  - `assetId` string: One of your assets, read by this service (its browser-readable rendition when it has one)
  - `url` string: An image on the internet, fetched by this service: `http(s)` only, never a private or loopback address, redirects not followed. Its format is the type it was served as, else the one its extension names
  - `parameters` object: Transform with `op` only: the op's parameters. A key also in the query string takes the query's value.

## Response `200`

The transformed image, `Content-Type` the produced format, with `X-ImageStep-Width` / `X-ImageStep-Height` and `X-ImageStep-Duration-Ms`. With `?response=url`, JSON instead: `data` is a signed link to the result, valid for 5 minutes.

- ApiResponseSyncUrlResult: Standard API response wrapper containing status, message, data and timestamp
  - `success` boolean, required: Indicates if the request was successful
  - `message` string: Human-readable message about the response
  - `data` SyncUrlResult, required: A transformed image as a signed link instead of the bytes
    - `url` string, required: Where to fetch the result; signed, valid for `expiresInSeconds`
    - `contentType` string, required: The result's type, e.g. image/webp
    - `bytes` integer, required: The result's size in bytes
    - `expiresInSeconds` integer, required: How long the link works, in seconds
    - `width` integer: The result's pixel width, when the worker reported it
    - `height` integer: The result's pixel height, when the worker reported it
  - `timestamp` string, date-time: Timestamp when the response was generated
  - `error` ApiError: Structured error. Present on every 4xx/5xx response, absent on success.
    - `code` string: Machine-readable error code from a closed set
    - `message` string: Human-readable explanation. Never parse this — parse `code`.
    - `retryable` boolean, required: Whether repeating the same request unchanged could succeed. The whole reason this field exists is that an agent cannot infer it from the status code.
    - `param` string: Which request parameter the error is about, when it is about one
    - `details` object: Extra machine-readable context, e.g. per-field validation messages
    - `requestId` string: The id of this request, identical to the `X-Request-Id` response header. Quote it when reporting a failure: it is what finds the request in our logs.
  - `meta` PageMeta
    - `total` integer: Rows matching the filter. Page mode only: a cursor request counts nothing
    - `page` integer: The page served, from 0. Page mode only
    - `perPage` integer: Rows per page actually used (out of range is clamped)
    - `hasMore` boolean: Whether there is another page
    - `nextCursor` string, nullable: Pass back as `cursor` for the rows after this page; null on the last page. Opaque — send it unchanged, with the same filters

## Other responses

- `400`: - `invalid_param` on `op` / `preset` — neither or both given; an unknown op; an op that has no synchronous form (an AI op, `overlay`) or is answered by another endpoint (`read_metadata`) - `invalid_param` on `preset` — a preset with an AI step, several segments, or a step that reads a stored layer - `invalid_param` naming the key — a parameter the op does not declare or cannot take, or any parameter beside `preset` - `invalid_param` on `file` — a multipart body with no `file` part - `invalid_param` on `url` — not `http(s)`, does not resolve, resolves to an address this service will not fetch, redirects, answers an HTTP error other than 5xx / 429 / 408, or returns nothing - `invalid_state` on `assetId` — the asset has no stored object yet - `unsupported_format` — not an image, a format this service does not read, or an input over 50 megapixels
- `404`: - `asset_not_found` — the JSON form's `assetId` is not one of your assets - `preset_not_found` — no such preset or version, or it is not yours
- `413`: `payload_too_large` on `file` — the image is over 25 MB; `details.limit` is the ceiling in bytes
- `415`: `unsupported_media_type` — a form-encoded body; `details.supported` lists what is read
- `429`: Rate limited — `error.code` is `rate_limited` and `retryable` is true. `Retry-After` says how many seconds to wait.
- `503`: `provider_unavailable`, retryable — `details.reason` says which: - `capacity` — this node, or the worker behind it, is full (`Retry-After`) - `deadline_exceeded` — the work took longer than the deadline (`Retry-After`) - `worker_unreachable` — no processing worker answered (`Retry-After`) - `storage_read` — the JSON form's stored asset could not be read (`Retry-After`) - `url_unavailable` — the `url`'s host answered 5xx, 429 or 408, or not in time (`Retry-After`)

---

[API](https://skmtc.dev/imagestep/apis/imagestep-api.md) · [All operations](https://skmtc.dev/imagestep/apis/imagestep-api/llms.txt) · [OpenAPI document](https://skmtc.dev/imagestep/apis/imagestep-api/revisions/5e8e922a3085?raw)
