Skip to content

Assets

The images you store. Three ways in — one call with the bytes as the body, the three-step upload for large files and batches, or a public URL this service fetches — then search, collections and tags to find them again, publishing for a public URL, and delete. A job never rewrites an asset: what it makes is a new one.

14 endpoints under /api/v1/assets. 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 Assets. * marks a required field.

List and search assets

GET /api/v1/assets

Paged — page and perPage, or cursor, in; meta out

Paginated list of the caller's assets, newest-created first. Every filter is optional and they compose: ?tag=cat&mime=image/jpeg&minWidth=2000.

Each row is an AssetSummary — what an asset is called, what it is, how it is labelled and published, and when it expires. The image facts, metadata and lineage are only on GET /api/v1/assets/{id}. To walk a whole library, follow meta.nextCursor as cursor rather than counting pages: a cursor page counts nothing. A chain job's intermediate products are left out unless includeIntermediate=true.

USE THIS WHEN:

  • Listing a view (all, published) or one collection
  • Filtering by tag, MIME type, pixel dimensions, capture date or source operation
  • Free-text search over the name and camera make/model

DO NOT USE WHEN:

  • Retrieving a single asset by ID → use GET /api/v1/assets/{id}
  • Listing the collections that exist → use GET /api/v1/assets/collections

Parameters

NameInTypeDescription
viewquerystringWhich slice of the library to read: ALL, or PUBLISHED for only the assets that carry a publicUrlone of ALL · PUBLISHED
collectionquerystringCollection (an opaque label you set on upload, on a job or with POST /api/v1/assets/update), matched exactly; omit for every collection
tagquerystringOne of your tags, matched exactly (case included) against the tags you set on upload or with POST /api/v1/assets/update
hasCollectionquerybooleanWhether the asset is in a collection at all: false is everything you have not filed, which no other filter can express. Cannot be combined with collection, which already says the asset is in one
mimequerystringMIME type as stored (image.mimeType), matched exactly, e.g. image/jpeg
jobIdquerystringThe job that produced these assets — everything one run left behind. An upload has no job and never matches
opquerystringThe op that produced these assets (their lineage.op), as GET /api/v1/ops names it. Recorded when the job was submitted with an op, and on each segment of a chain: an upload never matches, and neither does the output of a job submitted by type or by a one-segment preset
sourcequerystringWhere it came from: an upload, or the kind of job that made itone of upload · process · ai-generate · ai-edit · render
minWidthqueryinteger (int32)Pixel width lower bound (inclusive)
maxWidthqueryinteger (int32)Pixel width upper bound (inclusive)
minHeightqueryinteger (int32)Pixel height lower bound (inclusive)
maxHeightqueryinteger (int32)Pixel height upper bound (inclusive)
takenFromquerystringCapture date lower bound (EXIF DateTimeOriginal, the asset's takenAt). Epoch millis, or an ISO-8601 date or instant read as UTC: 2026-09-14, 2026-09-14T08:30:00Z. An image that records no capture date never matches a capture bound
takenToquerystringCapture date upper bound. Same forms as takenFrom; a bare date includes the whole of that day
createdFromquerystringCreated-at lower bound — when this service made or ingested the asset, which is the axis an automation's output lies along. Epoch millis, or an ISO-8601 date or instant read as UTC: 2026-09-14, 2026-09-14T08:30:00Z
createdToquerystringCreated-at upper bound. Same forms as createdFrom; a bare date includes the whole of that day
statusquerystringIngest state: PROCESSING while the worker is still reading the bytes, DONE once it can be processed or published, FAILED for an upload whose ingest never finished (a tombstone — it expires a day after it was made and does not count against your plan)one of PROCESSING · DONE · FAILED
qquerystringFree text: a substring of the name or of the camera make and model, case-insensitive
includeIntermediatequerybooleanInclude a chain job's intermediate products — the asset one segment made for the next to read (imagestep#246). They are out of the library by default: they exist for as long as the run does, and are deleted when their item settles.

Returns 200 — data is AssetSummary[]

One page of rows; meta carries hasMore and nextCursor

Errors

StatusCode and when
400
  • invalid_param on hasCollection — sent together with collection, which already says the asset is in one
  • invalid_param on status, takenFrom, takenTo, createdFrom or createdTo — a value it cannot read
  • invalid_param — source is not one of its values (this one names no param)
  • invalid_param on cursor — malformed, taken from another listing, or sent together with page

List your collections

GET /api/v1/assets/collections

Paged — page and perPage, or cursor, in; meta out

Every collection your assets are in, one row each: the name, how many assets are in it and when the latest one was created — most recently added to first. count is the total GET /api/v1/assets?collection= reports for that name. q narrows the list to names containing it, case-insensitively. A chain job's intermediates are not counted and their job: collections are not listed.

USE THIS WHEN:

  • Finding out which collections exist before filtering or submitting into one
  • Checking a collection name's spelling (a misspelt name is simply a new collection)

DO NOT USE WHEN:

  • Listing the assets in a collection → use GET /api/v1/assets?collection=

Parameters

NameInTypeDescription
qquerystringOnly names containing this text (case-insensitive)

Returns 200 — data is CollectionSummary[]

One page of collections; meta carries hasMore and nextCursor

Errors

StatusCode and when
400

invalid_param on cursor — malformed, taken from another listing, or sent together with page

Rename a collection

POST /api/v1/assets/collections/rename

Accepts an Idempotency-Key

Moves every one of your assets in from to to, in one statement — the way to rename a collection, or with to: "" to take its assets out of any collection. Renaming onto a name that is already in use merges the two. updated is how many assets moved; a name nothing is in moves nothing and is not an error. Both names are checked like any other collection name, and a chain job's job: collections cannot be renamed.

USE THIS WHEN:

  • Renaming, merging or dissolving a collection

DO NOT USE WHEN:

  • Moving some assets, not all of a collection → use POST /api/v1/assets/update with collection

Request body — RenameCollectionRequest

Returns 200 — data is CollectionRenamed

What moved

Errors

StatusCode and when
400
  • invalid_param on from — missing or empty, or not a name a collection can have (too long, a control character, starting with job:)
  • invalid_param on to — missing (send "" to take the assets out of any collection), or not a name a collection can have

Batch delete assets permanently

POST /api/v1/assets/delete

Accepts an Idempotency-Key

Deletes multiple assets and queues their stored objects for removal. Final — there is no trash and no restore. IDs that are not yours, or no longer exist, are skipped without an error: deleted lists what was actually deleted. A published asset's publicUrl stops answering within about two minutes.

USE THIS WHEN:

  • Deleting specific assets by IDs

DO NOT USE WHEN:

  • Letting assets lapse on their own → do nothing; each is deleted at its expiresAt

Request body — AssetBatchDeleteRequest

Returns 200 — data is BatchDeleteResultDTO

What was deleted

Errors

StatusCode and when
400

invalid_param — ids is missing, empty, or longer than 10000

Finalize file uploads

POST /api/v1/assets/finish-upload

Accepts an Idempotency-Key

Creates one asset per staged object whose bytes were PUT to the staged URL.

Send only what the caller knows: which staged object, the file's name and, optionally, a collection and tags — [{"objectId": "…", "name": "photo.jpg", "collection": "shoot-01"}]. The type comes from the name's extension (the rule stage-upload signed the PUT with); size, dimensions and SHA-1 are measured by ingest; the source is always upload. Every asset starts PROCESSING — poll POST /api/v1/assets/status until it is DONE.

Every item is checked before any is written, so one bad item fails the whole request and creates nothing. At most 500 objects per request. The asset limit is checked here, and this is the check that counts — stage-upload's is advisory.

USE THIS WHEN:

  • Finalizing file uploads after uploading to staged URLs (single or batch)

DO NOT USE WHEN:

  • Staging the upload → use POST /api/v1/assets/stage-upload
  • The image is at a public URL → use POST /api/v1/assets/from-url

Request body — FinishUploadRequest[]

Returns 200 — data is FinishedUpload[]

One new asset per object, in request order

Errors

StatusCode and when
400
  • invalid_param on objectId — missing, not staged by this account, named twice in the request, already finished, or staged more than a day ago (a staged object never finished is deleted after a day)
  • invalid_param on name — no image extension this service accepts
  • invalid_param on collection or tags — over a limit, a control character, or a collection starting with job:
  • invalid_param — an empty list, or more than 500 objects
422

asset_count_exceeded — the objects would take the account past its plan's asset limit

Ingest images from URLs

POST /api/v1/assets/from-url

Accepts an Idempotency-Key

Fetches each public image URL here, stores it and creates an asset — the same asset an upload creates, in PROCESSING until ingest has written dimensions and metadata.

One result per URL, in request order: {url, id, status, name} or {url, error} with the usual code / retryable / param. A private or unresolvable address, a URL that is not an image (unsupported_format), a body over the limit or a host that fails for now (provider_unavailable, retryable) fails that URL only. As on POST /api/v1/assets/upload, the same bytes are the same asset: bytes the account already holds as a DONE asset (same SHA-1) come back as that asset with existing: true, and so does a URL whose bytes an earlier URL of the same request already brought in — nothing new is stored, and a duplicate keeps its own name, collection and tags. So a URL that landed can be sent again; an Idempotency-Key still replays the whole answer, the rows that failed included.

LIMITS: at most 20 URLs per call; each body at most 25 MB; redirects are not followed; the asset count for all URLs is checked before anything is fetched. A response that declares a type that is not an image (and a URL with no image extension), or a length over the limit, is refused before its body is read. The call shares the synchronous endpoints' concurrency: over your account's → 429 rate_limited, the node full → 503 provider_unavailable; both retryable, with Retry-After.

USE THIS WHEN:

  • The image is somewhere on the internet and you want an asset_id for a job

DO NOT USE WHEN:

  • You hold the bytes → POST /api/v1/assets/upload (one file, up to 25 MB), or POST /api/v1/assets/stage-upload → PUT → finish-upload for larger files and batches
  • You only want one image transformed and back → POST /api/v1/images/transform with {"url": …}

Request body — FromUrlRequest

Returns 200 — data is FromUrlResult[]

One result per URL, in request order — a refused URL is a result with error, not a failed request; bytes you already hold are that asset, existing: true

Errors

StatusCode and when
400
  • invalid_param on urls — empty, or more than 20 URLs
  • invalid_param on collection or tags — over a limit, a control character, or a collection starting with job:
422

asset_count_exceeded on urls — more URLs than your plan's asset limit has room for; nothing is fetched

503

provider_unavailable — this node is at its concurrent-request capacity (details.reason is capacity). Retryable, after Retry-After

Sign preview URLs for a page of assets

POST /api/v1/assets/preview-urls

Signs URLs for the preview renditions (400 px wide WebP) of up to 100 of your assets in one call, each valid for 15 minutes (ttlSeconds), and reports each asset's ingest status alongside. IDs that are not yours are left out rather than reported. A read that is a POST only because a page of ids does not fit a query string, so it takes no Idempotency-Key — a replay would hand back stale signatures.

USE THIS WHEN:

  • Painting thumbnails for a listing page (one call per page, not one per image)

DO NOT USE WHEN:

  • Waiting for assets to finish ingest → POST /api/v1/assets/status (nothing signed, no audit row)
  • You want the full-size bytes → GET /api/v1/assets/{id}/content
  • You want a stable public URL → publish the asset and use publicUrl

Request body — PreviewUrlsRequest

Returns 200 — data is PreviewUrls

Errors

StatusCode and when
400

invalid_param on ids — empty, or more than 100 ids

Stage file upload

POST /api/v1/assets/stage-upload

Accepts an Idempotency-Key

Step one of the three-step upload (stage → PUT the bytes to each returned url → finish-upload). The bytes go straight to the object store, never through this API, which is what lets it take up to 500 files of up to 100 MB in one call.

Each url is signed for one PUT, valid for 60 minutes, with the contentType it answers (read from fileName's extension) and a Content-Length equal to the fileSize you declared — the object store refuses any other. A file this service does not take (no image extension it accepts, or over 100 MB) comes back with error set and no url; the rest of the batch is staged. A sha1Hash matching a DONE asset you already hold sets exists and existingAssetId: reuse that asset, or upload anyway. A staged object that is never finished is deleted after a day.

The asset limit is checked here against the files it would stage, and again by finish-upload, which is the check that counts.

USE THIS WHEN:

  • You have many files, or one over 25 MB (call this first, then PUT to the returned URL, then call finish-upload)

DO NOT USE WHEN:

  • You hold one image of up to 25 MB → POST /api/v1/assets/upload does it in one call
  • The image is at a public URL → POST /api/v1/assets/from-url
  • Finalizing an upload → use POST /api/v1/assets/finish-upload

Request body — StageUploadRequest[]

Returns 200 — data is StageUploadResponse[]

One entry per file, in request order: a signed url, or an error

Errors

StatusCode and when
400

invalid_param — an empty array, more than 500 files, or a file with no fileSize

422

asset_count_exceeded — the files it would stage are more than your plan's asset limit has room for

Poll the ingest status of up to 100 assets

POST /api/v1/assets/status

Where a batch of your assets is in ingest, in one call — the poll after an upload or a URL ingest, instead of GET /api/v1/assets/{id} per asset per tick. PROCESSING is still running; DONE and FAILED are final. width and height appear once ingest has measured them.

Nothing is signed and nothing is written to the audit log: the answer is your own assets' progress, and IDs that are not yours are absent rather than reported. A POST only because a batch of ids does not fit a query string, so it takes no Idempotency-Key — a replay would answer a stale status.

USE THIS WHEN:

  • Waiting for uploaded or URL-ingested assets to finish (poll every one or two seconds)

DO NOT USE WHEN:

  • You need the whole asset → GET /api/v1/assets/{id} once it is DONE
  • You want the bytes → GET /api/v1/assets/{id}/content

Request body — AssetStatusRequest

Returns 200 — data is AssetStatuses

Errors

StatusCode and when
400

invalid_param on ids — empty, or more than 100 ids

Batch update asset properties

POST /api/v1/assets/update

Accepts an Idempotency-Key

Updates the published status, the collection and the tags of multiple assets. Each field you send is applied to every listed asset; a field you omit is left alone. collection: "" takes the assets out of their collection; tags replaces the whole list ([] clears it). IDs that are not yours are skipped without an error: the answer is the assets that were updated, as full records with the change applied.

published: true gives each asset its publicUrl, the full-size image on the CDN; published: false takes it down within about two minutes.

USE THIS WHEN:

  • Publishing or unpublishing one or more assets
  • Putting assets in a different collection
  • Tagging assets so GET /api/v1/assets?tag= finds them again

DO NOT USE WHEN:

  • Permanently deleting assets → use POST /api/v1/assets/delete
  • Renaming a whole collection → use POST /api/v1/assets/collections/rename

Request body — AssetBatchUpdateRequest

Returns 200 — data is Asset[]

The updated assets, in request order

Errors

StatusCode and when
400
  • invalid_param on ids — missing, empty, or longer than 10000
  • invalid_param on collection or tags — over a limit, a control character, or a collection starting with job:

Upload one image

POST /api/v1/assets/upload

Stores the request body as an asset in one call: the bytes are the body, the parameters are the query string. Send the image as the raw body with its own Content-Type (image/jpeg, image/png, …) and a Content-Length, or as multipart/form-data with a file part. The asset is created in PROCESSING and reaches DONE once ingest has written dimensions and metadata — poll POST /api/v1/assets/status or subscribe to the webhook.

The type is the declared Content-Type when it is an image type this product takes, else the extension of name (or the multipart file name); neither → 400 unsupported_format. Bytes the account already holds as a DONE asset (same SHA-1) come back as that asset with existing: true and nothing new is stored.

LIMITS: one file per call, at most 25 MB (413 payload_too_large); a raw body needs Content-Length (400 invalid_param). Over this node's concurrent-upload capacity → 503 provider_unavailable with Retry-After, retryable. No Idempotency-Key (contract §3): the same bytes are the same asset, so re-sending is already safe.

USE THIS WHEN:

  • You hold one image of up to 25 MB and want an asset_id for a job

DO NOT USE WHEN:

  • The file is over 25 MB, or you have many → POST /api/v1/assets/stage-upload, PUT, then finish-upload (the bytes then go straight to the object store)
  • The image is at a public URL → POST /api/v1/assets/from-url
  • You only want one image transformed and back → POST /api/v1/images/transform

Parameters

NameInTypeDescription
namequerystringFile name. Its extension types the bytes when Content-Type does not; the asset is named after its stem. Defaults to the multipart file name, else image.
collectionquerystringCollection to put the new asset in (optional; at most 200 characters, and names starting with job: are reserved). Ignored when the bytes match an asset you already hold
tagsquerystring[]Tags for the new asset (optional; repeat the parameter or separate with commas; at most 50, each at most 100 characters). A duplicate of an asset you already hold keeps that asset's own tags.
retentionDaysqueryinteger (int32)Keep the new asset this many days instead of your plan's retention — shorter only; more is kept for the plan's time (imagestep#591). Its expiresAt says what was stamped.

Request body — image/* or multipart/form-data

The image: raw bytes with their own Content-Type, or a multipart form with a file part

Returns 200 — data is UploadedAsset

The asset: new (PROCESSING), or the DONE one you already hold for the same bytes (existing: true)

Errors

StatusCode and when
400
  • invalid_param on Content-Length — a raw body sent chunked, with no length
  • invalid_param on body — an empty body
  • invalid_param on file — a multipart request with no file part
  • invalid_param — a JSON body; to ingest by URL use POST /api/v1/assets/from-url (details.supported lists what this takes)
  • invalid_param on collection or tags — over a limit, a control character, or a collection starting with job:
  • unsupported_format — neither the Content-Type nor the name's extension is an image type this service takes
413

payload_too_large on file — over the 25 MB this endpoint takes; details.limit is the ceiling in bytes. Larger files take the three-step upload

415

unsupported_media_type — a form-encoded body, which is consumed before this endpoint can read it; details.supported lists what it takes

422

asset_count_exceeded — your plan's asset limit is reached

503

provider_unavailable — this node is at its concurrent-upload capacity (details.reason is capacity). Retryable, after Retry-After (2 s)

Get asset by ID

GET /api/v1/assets/{id}

One asset's full record: the list row's fields plus image (type, pixel size, bytes, SHA-1 and the facts ingest derived), metadata (what exiftool read), takenAt and, for anything a job made, lineage. While the asset is PROCESSING its image facts are not measured yet.

USE THIS WHEN:

  • Reading one asset's full record (image facts, metadata, lineage), typically once it is DONE
  • Resolving a known asset ID

DO NOT USE WHEN:

  • Listing assets → use GET /api/v1/assets
  • Waiting for ingest to finish → POST /api/v1/assets/status polls a batch in one call
  • You want the bytes → GET /api/v1/assets/{id}/content

Parameters

NameInTypeDescription
id (required)pathstringAsset ID

Returns 200 — data is Asset

The asset

Errors

StatusCode and when
404

asset_not_found — no such asset, or it is not yours

Delete an asset file permanently

DELETE /api/v1/assets/{id}

Accepts an Idempotency-Key

Deletes an asset and queues its stored objects for removal. There is no trash and no restore: the delete is final. A retry with the same Idempotency-Key replays the answer; without one, a second delete is 404 asset_not_found. A published asset's publicUrl stops answering within about two minutes.

USE THIS WHEN:

  • Permanently removing one asset file by ID

DO NOT USE WHEN:

  • Deleting multiple assets at once → use POST /api/v1/assets/delete
  • Letting an asset lapse on its own → do nothing; it is deleted at its expiresAt

Parameters

NameInTypeDescription
id (required)pathstringAsset ID

Returns 204 — no body

Deleted; no body

Errors

StatusCode and when
404

asset_not_found — no such asset, or it is not yours

Download asset content

GET /api/v1/assets/{id}/content

Redirects (302) to a signed URL for the asset's own bytes, valid for 5 minutes. The asset does NOT need to be published — this is the private read path. The redirect is Cache-Control: private, no-store: the URL is the credential, so follow it and do not keep it.

USE THIS WHEN:

  • Fetching your own asset's bytes from a script, SDK or CLI
  • Previewing an asset you do not want on a public URL

DO NOT USE WHEN:

  • You want a stable, cacheable, public URL → publish the asset (POST /api/v1/assets/update with published=true) and use publicUrl

Parameters

NameInTypeDescription
id (required)pathstringAsset ID
variantquerystringWhich stored form to download: readable (default; the image at full size in a type a browser shows — for HEIF, RAW, PSD and the like a rendition ingest writes, the original until it has), original (the bytes as uploaded or produced) or preview (a 400 px wide WebP)one of readable · original · preview

Returns 302 — no body

Redirect (Location) to the signed URL for the bytes. Follow it (curl -L).

Errors

StatusCode and when
400
  • invalid_param on variant — not one of readable, original, preview
  • invalid_state on variant — that rendition is not written yet: preview exists once ingest has run (the asset is DONE)
404

asset_not_found — no such asset, or it is not yours

Objects

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

Asset

One stored image: what it is called, what it is, how it is labelled and published, when it expires and, for a job's output, how it was made

FieldTypeDescription
collectionstring
The collection it is in — an opaque label you chose, matched exactly; absent when it is in none
createdAtinteger (int64)
Epoch millis; the library is listed newest-created first
expiresAtinteger (int64)
Epoch millis when the asset and its objects are deleted. Stamped at creation from your plan's retention and never recomputed, so a downgrade does not shorten it; an upload that fails ingest expires a day after it was created
idstring
Asset ID: ast_ and 32 hex digits
imageAssetImage
What the asset is, as ingest measured it
lineageAssetLineage
How a job made it — absent on an upload. Written with the asset and never changed
metadataobject
Everything the file says about itself, as exiftool read it, under exiftool's own tag names — the map POST /api/v1/images/metadata answers with. Absent until ingest has read it
namestring
Display name — an upload's file name without its extension. Not unique; id is what addresses the asset
publicUrlstring
The CDN URL of the full-size image, once published; absent otherwise. Unpublishing or deleting takes it down within about two minutes
publishedboolean
Whether it is on the public CDN — publicUrl is there when it is. Set with POST /api/v1/assets/update
sourcestring
Where it came from: upload, or the kind of job that made itone of upload · process · ai-generate · ai-edit · render
statusstring
PROCESSING while ingest reads the bytes; DONE once it has measured them — the asset can then be processed and published; FAILED for an upload ingest could not read, kept a day so you can see whyone of PROCESSING · DONE · FAILED
tagsstring[]
Your own labels, [] when there are none — what GET /api/v1/assets?tag= matches. A default analyze appends the tags and objects it found after yours
takenAtinteger (int64)
When the picture was taken, epoch millis — metadata.DateTimeOriginal, and what takenFrom / takenTo on GET /api/v1/assets filter. Absent when the image does not say
updatedAtinteger (int64)
Epoch millis of the last change you made to a label — name, collection, tags, published — or the creation time until then. Ingest and analysis do not move it

AssetBatchDeleteRequest

Batch delete request with the asset IDs to delete

FieldTypeDescription
ids (required)string[]
Asset IDs to delete permanently, at most 10000

AssetBatchUpdateRequest

Batch update request with asset IDs and properties to update

FieldTypeDescription
collectionstring
Collection to put the assets in; "" takes them out of theirs (at most 200 characters; names starting with job: are reserved)e.g. "shoot-01"
ids (required)string[]
Asset IDs to update, at most 10000
publishedboolean
true publishes every listed asset (it gets a publicUrl), false takes them down; omit to leave them as they are
tagsstring[]
Replace the assets' tags with this list ([] clears them; omit to leave them). Your own labels, matched exactly by GET /api/v1/assets?tag= (at most 50, each at most 100 characters)e.g. ["hero","spring-sale"]

AssetImage

What an asset is, as ingest measured it: type, pixel size, bytes, SHA-1, and the facts a caller would otherwise download the image to learn. A derived fact is absent when it could not be derived

FieldTypeDescription
animatedboolean
Whether it has more than one frame or page
blurhashstring
A BlurHash placeholder to paint while the image loads. It has no alpha channel: prefer thumbhash for an image with transparency
colorSpacestring
The colour space the file is in, e.g. srgb or cmyk (a CMYK original goes grey on the web)
densitynumber (double)
Dots per inch, when the file records it
dominantColorstring
The dominant colour, #rrggbb
hasAlphaboolean
Whether the file has an alpha channel (app stores refuse one)
hasIccProfileboolean
Whether the file embeds an ICC colour profile
heightinteger (int32)
Pixel height; 0 until ingest has measured it
mimeTypestring
The type it is stored as, e.g. image/jpeg
opaqueboolean
Whether every pixel is fully opaque — false only when the alpha channel is actually used
pagesinteger (int32)
Frame or page count; 1 for a still image
phashstring
64-bit perceptual hash, 16 hex digits: two images a few bits apart are the same picture, even re-encoded or resized. Meaningful for photographs, not flat graphics
sha1Hashstring
SHA-1 of the stored bytes, hex — the same bytes, not merely the same picture (that is phash)
sizeinteger (int64)
Stored size in bytes; 0 until ingest has measured it
thumbhashstring
A ThumbHash placeholder — smaller than blurhash, closer to the picture, and it keeps alpha
widthinteger (int32)
Pixel width; 0 until ingest has measured it

AssetLineage

The run that produced an asset: the job, what it ran, and what it was made from

FieldTypeDescription
intermediateboolean
true while it exists only to feed a chain's next segment: out of GET /api/v1/assets unless includeIntermediate=true, not counted against your asset limit, deleted when its item settles
jobIdstring
The job whose item made it
modelstring
The model that made it, for a model's output
opstring
The op that made it, when the job was submitted with one, or the op of the chain segment that made it
parametersobject
The model parameters it ran with
presetIdstring
The preset the job ran, when it ran one
presetVersioninteger (int32)
That preset's version at submit time — the steps that actually ran
promptstring
What that model was asked, every subject placeholder expanded
sourceAssetIdstring
The asset it was made from — always the image you sent, never an intermediate a chain made on the way. Absent for text-to-image and a template render
stepStep
Which segment of a chain made it; absent for a job that ran as one segment
templateIdstring
The render template that drew it, for a render job's output
templateVersioninteger (int32)
That template's version — the bytes it was drawn from

AssetStatus

One asset's ingest state

FieldTypeDescription
heightinteger (int32)
Pixel height, once ingest has measured it
idstring
Asset ID
statusstring
PROCESSING while ingest runs; DONE and FAILED are finalone of PROCESSING · DONE · FAILED
widthinteger (int32)
Pixel width, once ingest has measured it

AssetStatusRequest

Asset IDs to poll, at most 100

FieldTypeDescription
ids (required)string[]
Asset IDs to poll: at least one, at most 100; a repeated id is answered once

AssetStatuses

Ingest states for a batch of assets

FieldTypeDescription
itemsAssetStatus[]
One entry per asset of yours in the request, in request order; IDs that are not yours are simply absent

AssetSummary

One asset as a list row. GET /api/v1/assets/{id} has the rest: image facts, metadata, lineage.

FieldTypeDescription
collectionstring
The collection it is in, if any
createdAtinteger (int64)
Epoch millis; the list is ordered by it, newest first
expiresAtinteger (int64)
Epoch millis when the asset and its objects are deleted
heightinteger (int32)
image.height, once ingest measured it; absent before
idstring
Asset ID
mimeTypestring
image.mimeType
namestring
Display name — an upload's file name without its extension
publicUrlstring
The CDN URL once published
publishedboolean
Whether it is on the public CDN; publicUrl is there when it is
sizeinteger (int64)
image.size: the stored bytes
sourcestring
Where it came fromone of upload · process · ai-generate · ai-edit · render
statusstring
PROCESSING until ingest measured it, then DONE or FAILEDone of PROCESSING · DONE · FAILED
tagsstring[]
Your tags; [] when there are none
widthinteger (int32)
image.width, once ingest measured it; absent before

BatchDeleteResultDTO

Result of a batch delete operation

FieldTypeDescription
deletedstring[]
Identifiers of deleted items (IDs or slugs depending on the resource)
deletedCountinteger (int32)
Number of items deletede.g. 3

CollectionRenamed

What a rename did

FieldTypeDescription
fromstring
The old name
tostring
The new name; absent when the assets were taken out of their collection
updatedinteger (int32)
How many assets moved; 0 when nothing was in from

CollectionSummary

One collection: its name, how many of your assets are in it, and when the latest was added

FieldTypeDescription
collectionstring
The name, as GET /api/v1/assets?collection= takes ite.g. "shoot-01"
countinteger (int64)
Assets in it — the total GET /api/v1/assets?collection= reports
lastCreatedAtinteger (int64)
Epoch millis the newest asset in it was created; the list is ordered by it, newest first

FinishUploadRequest

One staged object to turn into an asset

FieldTypeDescription
collectionstring
Optional collection to put the asset in: an opaque name, no hierarchy, at most 200 characters; names starting with job: are reservede.g. "shoot-01"
name (required)string
The file's name; its extension decides the typee.g. "vacation-photo.jpg"
objectId (required)string
The objectId stage-upload returned for this file
retentionDaysinteger (int32)
Keep the asset this many days instead of your plan's retention — shorter only; more is kept for the plan's time (imagestep#591). Its expiresAt says what was stamped.e.g. 7
tagsstring[]
Optional tags for the new asset: your own labels, matched exactly by GET /api/v1/assets?tag= (at most 50, each at most 100 characters)e.g. ["hero","spring-sale"]

FinishedUpload

One asset finish-upload created

FieldTypeDescription
idstring
The new asset's ID
namestring
Its name: the file name without its extension
statusstring
PROCESSING until ingest has measured it — poll POST /api/v1/assets/statusone of PROCESSING · DONE · FAILED

FromUrlRequest

Images to ingest by URL

FieldTypeDescription
collectionstring
Collection to put every new asset in (optional; at most 200 characters, and names starting with job: are reserved).e.g. "shoot-01"
retentionDaysinteger (int32)
Keep every new asset this many days instead of your plan's retention — shorter only; more is kept for the plan's time (imagestep#591). Each asset's expiresAt says what was stamped.e.g. 7
tagsstring[]
Tags attached to every new asset (optional): your own labels, matched exactly by GET /api/v1/assets?tag= (at most 50, each at most 100 characters)e.g. ["hero"]
urls (required)string[]
Public http(s) image URLs, fetched by this service: at least one, at most 20.e.g. ["https://cdn.example.com/shots/hero.png"]

FromUrlResult

One URL's outcome: an asset (id, status, name, existing) or an error

FieldTypeDescription
errorItemError
Why this URL did not become an asset; absent when it did
existingboolean
true when the bytes were already an asset — a DONE one you held, or the one an earlier URL in this request became — and that asset is returned with nothing new stored. Absent when error is set
idstring
The asset's ID — new, or the one you already hold for the same bytes; absent when error is set
namestring
The asset's name: for a new one, the URL's last path segment without its extension, or image; absent when error is set
statusstring
PROCESSING for a new asset, until ingest has measured it; DONE for one you already held. Absent when error is set
urlstring
The URL as you sent it

ItemError

Why one URL did not become an asset — the same fields as an error response

FieldTypeDescription
codestring
A code from the closed set: invalid_param (not an http(s) URL, a private or unresolvable address, a redirect, an HTTP error other than the ones below, no body), provider_unavailable (the URL's host answered 5xx, 429 or 408, or not in time; retryable), unsupported_format (not an image), payload_too_large (over the size limit) or internal_error (storing it failed on our side; retryable)
messagestring
One sentence for a person; branch on code
paramstring
The offending parameter: url, or file for a body over the limit
retryableboolean
Whether the same URL may succeed if sent again

PreviewUrl

One asset's preview: a signed URL for its small rendition when it exists, plus enough state to know why it does not yet

FieldTypeDescription
expiresAtinteger | null (int64)
When url stops working, epoch millis; absent when url is
heightinteger | null (int32)
Pixel height, once known
idstring
Asset ID
statusstring
Ingest state; a preview exists once this is DONEone of PROCESSING · DONE · FAILED
urlstring | null
Signed URL for the preview, a 400 px wide WebP; absent while ingest is still running or after it failed
widthinteger | null (int32)
Pixel width, once known

PreviewUrls

Signed preview URLs for a page of assets

FieldTypeDescription
itemsPreviewUrl[]
One entry per asset of yours in the request, in request order; IDs that are not yours are simply absent
ttlSecondsinteger (int64)
How long each url is valid from the moment it was signed

PreviewUrlsRequest

Request object for signing preview URLs

FieldTypeDescription
ids (required)string[]
Asset IDs to sign: at least one, at most 100; a repeated id is answered once

RenameCollectionRequest

Rename a collection: every asset in from moves to to

FieldTypeDescription
from (required)string
The collection as it is named nowe.g. "shoot-01"
to (required)string
Its new name; "" takes the assets out of any collection (at most 200 characters; names starting with job: are reserved)e.g. "shoot-2026-09"

StageUploadRequest

Request object for staging file upload

FieldTypeDescription
fileNamestring
Original file name; its extension must be an image type this service accepts, and decides the Content-Type the PUT is signed fore.g. "photo.cr2"
fileSize (required)integer (int64)
Size of the file in bytes, at most 100 MB. Signed into the URL: the PUT must send exactly this Content-Lengthe.g. 1024000
sha1Hashstring
SHA-1 of the file, hex (optional): when it matches a DONE asset you already hold, the answer says soe.g. "356a192b7913b04c54574d18c28d46e6395428ab"

StageUploadResponse

Response object for staged upload containing pre-signed URL and duplicate detection info

FieldTypeDescription
contentTypestring
The Content-Type this presigned URL is signed for (#48). The PUT MUST send exactly this value, and a Content-Length equal to the fileSize you declared — the signature covers both, so any other value is rejected by the object store. It is derived from your file name; you do not choose it.e.g. "image/jpeg"
errorstring
Why this file was not staged (no image extension this service accepts, or over the 100 MB limit). When present, objectId and url are absent: skip the file
existingAssetIdstring
Id of the caller's existing asset with the same SHA1 (present only when exists=true)
existsboolean
True when the caller already has an asset with this SHA1. The objectId/url above are still a fresh, EMPTY upload slot: either PUT the bytes and finish-upload as usual, or skip both and use existingAssetId.
objectIdstring
The staged object's key — <your account id>/<uuid>. Opaque: send it back to finish-upload unchanged. Absent when error is set
sha1Hashstring
The sha1Hash you sent, echoed so you can match the answer to your filee.g. "356a192b7913b04c54574d18c28d46e6395428ab"
urlstring
Pre-signed URL to PUT the bytes to, valid for 60 minutes. Absent when error is set

Step

Which segment of a chain produced the asset

FieldTypeDescription
countinteger (int32)
How many segments the chain has
indexinteger (int32)
The segment, 0-based

UploadedAsset

The asset one upload created, or the one the account already held for the same bytes

FieldTypeDescription
existingboolean
true when the account already had a DONE asset with the same SHA-1 — that asset is returned and nothing new is stored
idstring
Asset id
namestring
Asset name (the file name without its extension)
statusstring
PROCESSING for a new asset; DONE when existing is true