Skip to content

Synchronous images

Run a deterministic operation while you wait. Nothing is stored.

3 endpoints under /api/v1/images. What every call shares is on the REST overview, and what they are for — with the calls for every SDK, the CLI and MCP — on Synchronous ops. * marks a required field.

Read an image's metadata synchronously

POST /api/v1/images/metadata

EXIF, GPS, dimensions, format and SHA-1 for the image in the request body. Nothing is stored, no credits are spent, and it does NOT count against the plan's processing allowance — it reads a header.

The image is sent the ways /images/transform takes it: raw bytes with their own Content-Type, a multipart file part, or JSON {assetId} / {url}.

Returns the same image / metadata shape GET /api/v1/assets/{id} carries, so measuring an image before storing it and reading one after storing it hand back the same fields.

LIMITS: the transform lane's — 25 MB, 50 megapixels, 10 s — and its 4 calls in flight per account (429 rate_limited, Retry-After: 1).

USE THIS WHEN:

  • You want to know what you are holding before deciding whether to store it

DO NOT USE WHEN:

  • The image is already an asset → GET /api/v1/assets/{id} already has this

Request body — image/*, multipart/form-data or application/json (SyncReferenceInput)

The image as raw bytes with their own Content-Type, a multipart form with a file part, or a JSON reference (assetId or url) instead of bytes

Returns 200 — data is object

data is {image, metadata, durationMs}: image has the fields of an asset's image (mime type, dimensions, size, SHA-1 and what is derived from the pixels), metadata the map exiftool read under its own tag names, durationMs the worker's time.

Errors

StatusCode and when
400
  • unsupported_format — not an image, a format this service does not read, or an input over 50 megapixels
  • invalid_param on file / url — a multipart body with no file part; a url this service will not fetch, or one that answers an HTTP error other than 5xx / 429 / 408
  • invalid_state on assetId — the asset has no stored object yet
404

asset_not_found — the JSON form's assetId is not one of your assets

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

503

provider_unavailable, retryable, with Retry-After — details.reason is capacity, deadline_exceeded or worker_unreachable, storage_read for the JSON form's stored asset, or url_unavailable when the url's host answered 5xx, 429 or 408, or not in time.

Render one template row synchronously

POST /api/v1/images/render

A template plus ONE row of data becomes one PNG, returned directly. Nothing is stored: no job, no asset, no public URL.

templateId accepts id@version to pin a version, exactly as the job form does.

LIMITS: 20 s of wall clock (503, deadline_exceeded). It shares the synchronous lane's 4 calls in flight per account (429 rate_limited, Retry-After: 1), and a success counts one against the processing allowance — past it, paid from the balance (402 insufficient_credit when it cannot be). No Idempotency-Key: send it again.

USE THIS WHEN:

  • You want one image from one row of data, right now

DO NOT USE WHEN:

  • You have many rows → POST /api/v1/jobs {"op":"render_template", "items":[…]}
  • You want an asset id or a permanent URL → the job form

Request body — RenderRequest

Returns 200 — image/png

The PNG, at the template's width × height, with X-ImageStep-Duration-Ms

Errors

StatusCode and when
400

invalid_param on templateId — missing, an @version that is not a positive number, or a template Chromium could not load (not retryable, as in a batch)

404

not_found on templateId — no such template or version, or it is not yours

503

provider_unavailable, retryable, with Retry-After — details.reason says which:

  • capacity — this node, or the render worker, is full
  • deadline_exceeded — the render took longer than the deadline
  • worker_unreachable — no render worker answered

Transform an image synchronously

POST /api/v1/images/transform

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

Parameters

NameInTypeDescription
opquerystringOp name; must be one whose GET /api/v1/ops entry has a syncEndpoint. Mutually exclusive with preset.
presetquerystringA deterministic preset to run as one call: its slug or id, or slug@version to pin one version. Mutually exclusive with op.
responsequerystringurl to get JSON with a signed link to the result (valid 5 minutes) instead of the bytes; anything else, or nothing, answers the bytes. The one part of this endpoint that writes storage: a temporary object, gone within a day.

Request body — image/*, multipart/form-data or application/json (SyncReferenceInput)

The image as raw bytes with their own Content-Type, a multipart form with a file part, or a JSON reference (assetId or url) instead of bytes

Returns 200 — application/json or image/*

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 {url, contentType, bytes, expiresInSeconds, width, height} — a signed link to the result, valid for 5 minutes.

Errors

StatusCode and when
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

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)

Objects

Each described once; a linked type is another object on this page.

RenderRequest

One template and one row of data

FieldTypeDescription
dataobject
The row: variable name → value. {{ name }} is filled HTML-escaped, {{{ name }}} raw, a.b reads nested data; a name the row does not carry renders as nothing.
templateIdstring
Template id, your own slug, or either with @version to pin one version (required)e.g. "builtin-template-og-image"

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.

FieldTypeDescription
assetIdstring
One of your assets, read by this service (its browser-readable rendition when it has one)
opstring
Transform only: the op, as ?op=
parametersobject
Transform with op only: the op's parameters. A key also in the query string takes the query's value.
presetstring
Transform only: the preset, as ?preset=
urlstring
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