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
| Status | Code and when |
|---|---|
| 400 |
|
| 404 |
|
| 413 |
|
| 415 |
|
| 503 |
|
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
| Status | Code and when |
|---|---|
| 400 |
|
| 404 |
|
| 503 |
|
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
| Name | In | Type | Description |
|---|---|---|---|
| op | query | string | Op name; must be one whose GET /api/v1/ops entry has a syncEndpoint. Mutually exclusive with preset. |
| preset | query | string | A deterministic preset to run as one call: its slug or id, or slug@version to pin one version. Mutually exclusive with op. |
| response | query | string | url 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
| Status | Code and when |
|---|---|
| 400 |
|
| 404 |
|
| 413 |
|
| 415 |
|
| 503 |
|
Objects
Each described once; a linked type is another object on this page.
RenderRequest
One template and one row of data
| Field | Type | Description |
|---|---|---|
| data | object | 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. |
| templateId | string | 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.
| Field | Type | Description |
|---|---|---|
| assetId | string | One of your assets, read by this service (its browser-readable rendition when it has one) |
| op | string | Transform only: the op, as ?op= |
| parameters | object | Transform with op only: the op's parameters. A key also in the query string takes the query's value. |
| preset | string | Transform only: the preset, as ?preset= |
| 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 |