---
title: Assets
url: https://base_url.placeholder/docs/api/assets
group: Surfaces
---

# 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](https://base_url.placeholder/docs/api#conventions), and what they are for — with the calls for every SDK, the CLI and MCP — on [Assets](https://base_url.placeholder/docs/assets). `*` marks a required field.

- GET /api/v1/assets — List and search assets
- GET /api/v1/assets/collections — List your collections
- POST /api/v1/assets/collections/rename — Rename a collection
- POST /api/v1/assets/delete — Batch delete assets permanently
- POST /api/v1/assets/finish-upload — Finalize file uploads
- POST /api/v1/assets/from-url — Ingest images from URLs
- POST /api/v1/assets/preview-urls — Sign preview URLs for a page of assets
- POST /api/v1/assets/stage-upload — Stage file upload
- POST /api/v1/assets/status — Poll the ingest status of up to 100 assets
- POST /api/v1/assets/update — Batch update asset properties
- POST /api/v1/assets/upload — Upload one image
- GET /api/v1/assets/{id} — Get asset by ID
- DELETE /api/v1/assets/{id} — Delete an asset file permanently
- GET /api/v1/assets/{id}/content — Download asset content

## List and search assets

GET /api/v1/assets

[Paged](https://base_url.placeholder/docs/errors#pagination) — `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

| Name | In | Type | Description |
| --- | --- | --- | --- |
| view | query | string | Which slice of the library to read: ALL, or PUBLISHED for only the assets that carry a publicUrl<br>one of ALL · PUBLISHED |
| collection | query | string | Collection (an opaque label you set on upload, on a job or with POST /api/v1/assets/update), matched exactly; omit for every collection |
| tag | query | string | One of your tags, matched exactly (case included) against the tags you set on upload or with POST /api/v1/assets/update |
| hasCollection | query | boolean | Whether 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 |
| mime | query | string | MIME type as stored (image.mimeType), matched exactly, e.g. image/jpeg |
| jobId | query | string | The job that produced these assets — everything one run left behind. An upload has no job and never matches |
| op | query | string | The 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 |
| source | query | string | Where it came from: an upload, or the kind of job that made it<br>one of upload · process · ai-generate · ai-edit · render |
| minWidth | query | integer (int32) | Pixel width lower bound (inclusive) |
| maxWidth | query | integer (int32) | Pixel width upper bound (inclusive) |
| minHeight | query | integer (int32) | Pixel height lower bound (inclusive) |
| maxHeight | query | integer (int32) | Pixel height upper bound (inclusive) |
| takenFrom | query | string | Capture 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 |
| takenTo | query | string | Capture date upper bound. Same forms as takenFrom; a bare date includes the whole of that day |
| createdFrom | query | string | Created-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 |
| createdTo | query | string | Created-at upper bound. Same forms as createdFrom; a bare date includes the whole of that day |
| status | query | string | Ingest 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)<br>one of PROCESSING · DONE · FAILED |
| q | query | string | Free text: a substring of the name or of the camera make and model, case-insensitive |
| includeIntermediate | query | boolean | Include 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

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

## List your collections

GET /api/v1/assets/collections

[Paged](https://base_url.placeholder/docs/errors#pagination) — `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

| Name | In | Type | Description |
| --- | --- | --- | --- |
| q | query | string | Only names containing this text (case-insensitive) |

### Returns `200` — `data` is `CollectionSummary[]`

One page of collections; `meta` carries `hasMore` and `nextCursor`

### Errors

| Status | Code 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`](https://base_url.placeholder/docs/errors#idempotency)

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

| Status | Code 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:`)<br>- `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`](https://base_url.placeholder/docs/errors#idempotency)

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

| Status | Code 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`](https://base_url.placeholder/docs/errors#idempotency)

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

| Status | Code 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)<br>- `invalid_param` on `name` — no image extension this service accepts<br>- `invalid_param` on `collection` or `tags` — over a limit, a control character, or a collection starting with `job:`<br>- `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`](https://base_url.placeholder/docs/errors#idempotency)

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

| Status | Code and when |
| --- | --- |
| 400 | - `invalid_param` on `urls` — empty, or more than 20 URLs<br>- `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

| Status | Code 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`](https://base_url.placeholder/docs/errors#idempotency)

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

| Status | Code 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

| Status | Code 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`](https://base_url.placeholder/docs/errors#idempotency)

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

| Status | Code and when |
| --- | --- |
| 400 | - `invalid_param` on `ids` — missing, empty, or longer than 10000<br>- `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

| Name | In | Type | Description |
| --- | --- | --- | --- |
| name | query | string | File 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`. |
| collection | query | string | Collection 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 |
| tags | query | string[] | 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. |
| retentionDays | query | integer (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

| Status | Code and when |
| --- | --- |
| 400 | - `invalid_param` on `Content-Length` — a raw body sent chunked, with no length<br>- `invalid_param` on `body` — an empty body<br>- `invalid_param` on `file` — a multipart request with no `file` part<br>- `invalid_param` — a JSON body; to ingest by URL use POST /api/v1/assets/from-url (`details.supported` lists what this takes)<br>- `invalid_param` on `collection` or `tags` — over a limit, a control character, or a collection starting with `job:`<br>- `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

| Name | In | Type | Description |
| --- | --- | --- | --- |
| id* | path | string | Asset ID |

### Returns `200` — `data` is `Asset`

The asset

### Errors

| Status | Code 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`](https://base_url.placeholder/docs/errors#idempotency)

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

| Name | In | Type | Description |
| --- | --- | --- | --- |
| id* | path | string | Asset ID |

### Returns `204` — no body

Deleted; no body

### Errors

| Status | Code 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

| Name | In | Type | Description |
| --- | --- | --- | --- |
| id* | path | string | Asset ID |
| variant | query | string | Which 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)<br>one of readable · original · preview |

### Returns `302` — no body

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

### Errors

| Status | Code and when |
| --- | --- |
| 400 | - `invalid_param` on `variant` — not one of readable, original, preview<br>- `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

| Field | Type | Description |
| --- | --- | --- |
| collection | string | The collection it is in — an opaque label you chose, matched exactly; absent when it is in none |
| createdAt | integer (int64) | Epoch millis; the library is listed newest-created first |
| expiresAt | integer (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 |
| id | string | Asset ID: `ast_` and 32 hex digits |
| image | AssetImage | What the asset is, as ingest measured it |
| lineage | AssetLineage | How a job made it — absent on an upload. Written with the asset and never changed |
| metadata | object | 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 |
| name | string | Display name — an upload's file name without its extension. Not unique; `id` is what addresses the asset |
| publicUrl | string | The CDN URL of the full-size image, once published; absent otherwise. Unpublishing or deleting takes it down within about two minutes |
| published | boolean | Whether it is on the public CDN — `publicUrl` is there when it is. Set with POST /api/v1/assets/update |
| source | string | Where it came from: `upload`, or the kind of job that made it<br>one of upload · process · ai-generate · ai-edit · render |
| status | string | 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 why<br>one of PROCESSING · DONE · FAILED |
| tags | string[] | 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 |
| takenAt | integer (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 |
| updatedAt | integer (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

| Field | Type | Description |
| --- | --- | --- |
| ids* | string[] | Asset IDs to delete permanently, at most 10000 |

### AssetBatchUpdateRequest

Batch update request with asset IDs and properties to update

| Field | Type | Description |
| --- | --- | --- |
| collection | string | Collection to put the assets in; "" takes them out of theirs (at most 200 characters; names starting with job: are reserved)<br>e.g. "shoot-01" |
| ids* | string[] | Asset IDs to update, at most 10000 |
| published | boolean | true publishes every listed asset (it gets a publicUrl), false takes them down; omit to leave them as they are |
| tags | string[] | 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)<br>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

| Field | Type | Description |
| --- | --- | --- |
| animated | boolean | Whether it has more than one frame or page |
| blurhash | string | A BlurHash placeholder to paint while the image loads. It has no alpha channel: prefer thumbhash for an image with transparency |
| colorSpace | string | The colour space the file is in, e.g. srgb or cmyk (a CMYK original goes grey on the web) |
| density | number (double) | Dots per inch, when the file records it |
| dominantColor | string | The dominant colour, #rrggbb |
| hasAlpha | boolean | Whether the file has an alpha channel (app stores refuse one) |
| hasIccProfile | boolean | Whether the file embeds an ICC colour profile |
| height | integer (int32) | Pixel height; 0 until ingest has measured it |
| mimeType | string | The type it is stored as, e.g. image/jpeg |
| opaque | boolean | Whether every pixel is fully opaque — false only when the alpha channel is actually used |
| pages | integer (int32) | Frame or page count; 1 for a still image |
| phash | string | 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 |
| sha1Hash | string | SHA-1 of the stored bytes, hex — the same bytes, not merely the same picture (that is phash) |
| size | integer (int64) | Stored size in bytes; 0 until ingest has measured it |
| thumbhash | string | A ThumbHash placeholder — smaller than blurhash, closer to the picture, and it keeps alpha |
| width | integer (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

| Field | Type | Description |
| --- | --- | --- |
| intermediate | boolean | 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 |
| jobId | string | The job whose item made it |
| model | string | The model that made it, for a model's output |
| op | string | The op that made it, when the job was submitted with one, or the op of the chain segment that made it |
| parameters | object | The model parameters it ran with |
| presetId | string | The preset the job ran, when it ran one |
| presetVersion | integer (int32) | That preset's version at submit time — the steps that actually ran |
| prompt | string | What that model was asked, every subject placeholder expanded |
| sourceAssetId | string | 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 |
| step | Step | Which segment of a chain made it; absent for a job that ran as one segment |
| templateId | string | The render template that drew it, for a render job's output |
| templateVersion | integer (int32) | That template's version — the bytes it was drawn from |

### AssetStatus

One asset's ingest state

| Field | Type | Description |
| --- | --- | --- |
| height | integer (int32) | Pixel height, once ingest has measured it |
| id | string | Asset ID |
| status | string | PROCESSING while ingest runs; DONE and FAILED are final<br>one of PROCESSING · DONE · FAILED |
| width | integer (int32) | Pixel width, once ingest has measured it |

### AssetStatusRequest

Asset IDs to poll, at most 100

| Field | Type | Description |
| --- | --- | --- |
| ids* | 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

| Field | Type | Description |
| --- | --- | --- |
| items | AssetStatus[] | 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.

| Field | Type | Description |
| --- | --- | --- |
| collection | string | The collection it is in, if any |
| createdAt | integer (int64) | Epoch millis; the list is ordered by it, newest first |
| expiresAt | integer (int64) | Epoch millis when the asset and its objects are deleted |
| height | integer (int32) | image.height, once ingest measured it; absent before |
| id | string | Asset ID |
| mimeType | string | image.mimeType |
| name | string | Display name — an upload's file name without its extension |
| publicUrl | string | The CDN URL once published |
| published | boolean | Whether it is on the public CDN; `publicUrl` is there when it is |
| size | integer (int64) | image.size: the stored bytes |
| source | string | Where it came from<br>one of upload · process · ai-generate · ai-edit · render |
| status | string | PROCESSING until ingest measured it, then DONE or FAILED<br>one of PROCESSING · DONE · FAILED |
| tags | string[] | Your tags; [] when there are none |
| width | integer (int32) | image.width, once ingest measured it; absent before |

### BatchDeleteResultDTO

Result of a batch delete operation

| Field | Type | Description |
| --- | --- | --- |
| deleted | string[] | Identifiers of deleted items (IDs or slugs depending on the resource) |
| deletedCount | integer (int32) | Number of items deleted<br>e.g. 3 |

### CollectionRenamed

What a rename did

| Field | Type | Description |
| --- | --- | --- |
| from | string | The old name |
| to | string | The new name; absent when the assets were taken out of their collection |
| updated | integer (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

| Field | Type | Description |
| --- | --- | --- |
| collection | string | The name, as GET /api/v1/assets?collection= takes it<br>e.g. "shoot-01" |
| count | integer (int64) | Assets in it — the total GET /api/v1/assets?collection= reports |
| lastCreatedAt | integer (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

| Field | Type | Description |
| --- | --- | --- |
| collection | string | Optional collection to put the asset in: an opaque name, no hierarchy, at most 200 characters; names starting with job: are reserved<br>e.g. "shoot-01" |
| name* | string | The file's name; its extension decides the type<br>e.g. "vacation-photo.jpg" |
| objectId* | string | The objectId stage-upload returned for this file |
| retentionDays | integer (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.<br>e.g. 7 |
| tags | string[] | 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)<br>e.g. ["hero","spring-sale"] |

### FinishedUpload

One asset finish-upload created

| Field | Type | Description |
| --- | --- | --- |
| id | string | The new asset's ID |
| name | string | Its name: the file name without its extension |
| status | string | PROCESSING until ingest has measured it — poll POST /api/v1/assets/status<br>one of PROCESSING · DONE · FAILED |

### FromUrlRequest

Images to ingest by URL

| Field | Type | Description |
| --- | --- | --- |
| collection | string | Collection to put every new asset in (optional; at most 200 characters, and names starting with job: are reserved).<br>e.g. "shoot-01" |
| retentionDays | integer (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.<br>e.g. 7 |
| tags | string[] | 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)<br>e.g. ["hero"] |
| urls* | string[] | Public http(s) image URLs, fetched by this service: at least one, at most 20.<br>e.g. ["https://cdn.example.com/shots/hero.png"] |

### FromUrlResult

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

| Field | Type | Description |
| --- | --- | --- |
| error | ItemError | Why this URL did not become an asset; absent when it did |
| existing | boolean | 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 |
| id | string | The asset's ID — new, or the one you already hold for the same bytes; absent when `error` is set |
| name | string | The asset's name: for a new one, the URL's last path segment without its extension, or `image`; absent when `error` is set |
| status | string | PROCESSING for a new asset, until ingest has measured it; DONE for one you already held. Absent when `error` is set |
| url | string | The URL as you sent it |

### ItemError

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

| Field | Type | Description |
| --- | --- | --- |
| code | string | 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) |
| message | string | One sentence for a person; branch on `code` |
| param | string | The offending parameter: `url`, or `file` for a body over the limit |
| retryable | boolean | 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

| Field | Type | Description |
| --- | --- | --- |
| expiresAt | integer \| null (int64) | When `url` stops working, epoch millis; absent when `url` is |
| height | integer \| null (int32) | Pixel height, once known |
| id | string | Asset ID |
| status | string | Ingest state; a preview exists once this is DONE<br>one of PROCESSING · DONE · FAILED |
| url | string \| null | Signed URL for the preview, a 400 px wide WebP; absent while ingest is still running or after it failed |
| width | integer \| null (int32) | Pixel width, once known |

### PreviewUrls

Signed preview URLs for a page of assets

| Field | Type | Description |
| --- | --- | --- |
| items | PreviewUrl[] | One entry per asset of yours in the request, in request order; IDs that are not yours are simply absent |
| ttlSeconds | integer (int64) | How long each `url` is valid from the moment it was signed |

### PreviewUrlsRequest

Request object for signing preview URLs

| Field | Type | Description |
| --- | --- | --- |
| ids* | 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`

| Field | Type | Description |
| --- | --- | --- |
| from* | string | The collection as it is named now<br>e.g. "shoot-01" |
| to* | string | Its new name; "" takes the assets out of any collection (at most 200 characters; names starting with job: are reserved)<br>e.g. "shoot-2026-09" |

### StageUploadRequest

Request object for staging file upload

| Field | Type | Description |
| --- | --- | --- |
| fileName | string | Original file name; its extension must be an image type this service accepts, and decides the Content-Type the PUT is signed for<br>e.g. "photo.cr2" |
| fileSize* | integer (int64) | Size of the file in bytes, at most 100 MB. Signed into the URL: the PUT must send exactly this Content-Length<br>e.g. 1024000 |
| sha1Hash | string | SHA-1 of the file, hex (optional): when it matches a DONE asset you already hold, the answer says so<br>e.g. "356a192b7913b04c54574d18c28d46e6395428ab" |

### StageUploadResponse

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

| Field | Type | Description |
| --- | --- | --- |
| contentType | string | 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.<br>e.g. "image/jpeg" |
| error | string | 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 |
| existingAssetId | string | Id of the caller's existing asset with the same SHA1 (present only when exists=true) |
| exists | boolean | 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. |
| objectId | string | The staged object's key — `<your account id>/<uuid>`. Opaque: send it back to finish-upload unchanged. Absent when `error` is set |
| sha1Hash | string | The sha1Hash you sent, echoed so you can match the answer to your file<br>e.g. "356a192b7913b04c54574d18c28d46e6395428ab" |
| url | string | 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

| Field | Type | Description |
| --- | --- | --- |
| count | integer (int32) | How many segments the chain has |
| index | integer (int32) | The segment, 0-based |

### UploadedAsset

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

| Field | Type | Description |
| --- | --- | --- |
| existing | boolean | true when the account already had a DONE asset with the same SHA-1 — that asset is returned and nothing new is stored |
| id | string | Asset id |
| name | string | Asset name (the file name without its extension) |
| status | string | PROCESSING for a new asset; DONE when `existing` is true |
