Create an asset entity
POST /assets
Creates a new asset, optionally linking it to a previously
uploaded byte stream via file_hash. If file_hash is set,
the storage layer is re-pinned under
asset:<new-asset-id> and the caller’s prior user: pin
for that hash is dropped (the bytes are now owned by the
asset, not the uploader).
Authorizations
Section titled “Authorizations ”Request Body required
Section titled “Request Body required ”object
Sha256 of a previously uploaded storage object. When set,
the storage layer is re-pinned under asset:<new-id> and
the caller’s user: pin (from the prior upload) is
dropped.
object
Optional workflow state. Must reference a row in
workflow_state whose domain = 'asset'. When omitted,
the asset is placed in the asset domain’s initial state
(or NULL if the domain has none).
Responses
Section titled “ Responses ”Asset already exists with the same content hash and
the operator’s upload.dedup_behavior is warn —
returns the EXISTING asset (no new row created) with
a duplicate_warning field set so the UI surfaces a
dialog. See AssetWithDedup.duplicate_warning.
Asset payload extended with
a duplicate_warning field. Returned from POST /assets
with HTTP 200 when the operator’s upload.dedup_behavior
is warn and a duplicate hash was detected: the EXISTING
asset row is returned (no new asset created), and the
field signals the UI to surface a “this file was already
uploaded as X” dialog.
object
Sha256 of the linked storage object
Flat tag value list — backwards-compatible projection
that pre-1.14.B consumers depend on. New consumers
prefer tag_details below which carries per-tag source
- confidence + provenance.
Per-tag source/confidence/provenance.
Same set of tags as tags, ordered the same way; this
field is the typed projection the frontend
AssetTagBadge renders.
One tag with full source/confidence/provenance metadata. Backs the per-source UI badge.
object
The tag value itself (e.g. “kittens”).
Where the tag came from. manual = operator typed it
in the UI; ai = generated by an AI tag job;
import = bulk CSV / EXIF / upstream system. AI runs
never overwrite manual or import rows.
Provider-supplied confidence in [0, 1] when source=‘ai’; null for manual and import tags. UI filter threshold lives in ai.tag.confidence_threshold (default 0.5).
Provider slug (openai / claude / ollama) for source=‘ai’.
Model identifier (gpt-4o / claude-3-5-sonnet) for source=‘ai’.
Free-form JSONB metadata blob (EXIF, custom fields, etc.).
object
Optional workflow state (domain = ‘asset’).
Post-upload processing pipeline state. pending means
background jobs (variant generation, EXIF extraction,
etc.) are still in flight — the original file at /file
is always available, but /variants/{key} URLs may
404 until processing completes. ready is the steady
state. failed surfaces in the UI as a retry affordance.
True iff a servable col thumbnail variant exists for this
asset AND the caller passes the content plane
(visibility.CanReadContent, ADR 0064). The client renders the
thumbnail only when this is true and otherwise shows a
placeholder WITHOUT requesting bytes — so a content-gated or
preview-less asset never fires a /variants/col (or /file)
request that would 404 and spam the console (#471). A
restricted asset the caller may not read reads identically to
“no preview” (false), so this never confirms restricted
(0064-safe).
Base64-encoded thumbhash (~30 bytes decoded) for instant blurred placeholder rendering on feed cards. Computed synchronously at upload for image assets. NULL for non-image resources.
Soft-delete timestamp. Non-null only on rows surfaced by
the admin include_deleted=true listing (the trash view);
null on live rows.
Optional reason captured at soft-delete time.
WebVTT subtitle / caption tracks attached to this asset. Empty array when none, or when the asset’s renderable kind doesn’t support subtitles (image, 3D, document, etc.). Tracks ride along with the asset; they do NOT appear in asset-count queries and do NOT federate as standalone activities.
One WebVTT subtitle track attached to an asset. NOT a first-class asset; rides along with its parent in the Asset.subtitle_tracks array. Tracks are uniquely identified by (asset_id, lang) — re-uploading the same lang replaces the existing row rather than creating a duplicate.
object
RFC 5646 / BCP 47 language tag (e.g. “en”, “en-US”, “ja”, “fr-CA”). The sentinel value “und” is reserved for sidecar files that matched by basename but had no inferrable language segment.
Optional human-readable label (“English (US)”, “Forced”, “Director’s commentary”). Empty string when not provided; never null.
CAS hash of the stored WebVTT file.
The format the track was uploaded in. The conversion worker stores WebVTT regardless; tracking the source lets operators re-convert if a converter bug surfaces.
1.0 for text-based sources (deterministic conversion). <1.0 for OCR’d bitmap sources like IDX (DVD subtitles). UI MAY surface a warning banner below 0.8.
object
The asset id the caller is being redirected
to (same as id at the top level — duplicated
here so client code can branch on the
presence of duplicate_warning without
re-reading the top-level id).
Human-readable warning surfaced in the UI dedup dialog.
Asset created
object
Sha256 of the linked storage object
Flat tag value list — backwards-compatible projection
that pre-1.14.B consumers depend on. New consumers
prefer tag_details below which carries per-tag source
- confidence + provenance.
Per-tag source/confidence/provenance.
Same set of tags as tags, ordered the same way; this
field is the typed projection the frontend
AssetTagBadge renders.
One tag with full source/confidence/provenance metadata. Backs the per-source UI badge.
object
The tag value itself (e.g. “kittens”).
Where the tag came from. manual = operator typed it
in the UI; ai = generated by an AI tag job;
import = bulk CSV / EXIF / upstream system. AI runs
never overwrite manual or import rows.
Provider-supplied confidence in [0, 1] when source=‘ai’; null for manual and import tags. UI filter threshold lives in ai.tag.confidence_threshold (default 0.5).
Provider slug (openai / claude / ollama) for source=‘ai’.
Model identifier (gpt-4o / claude-3-5-sonnet) for source=‘ai’.
Free-form JSONB metadata blob (EXIF, custom fields, etc.).
object
Optional workflow state (domain = ‘asset’).
Post-upload processing pipeline state. pending means
background jobs (variant generation, EXIF extraction,
etc.) are still in flight — the original file at /file
is always available, but /variants/{key} URLs may
404 until processing completes. ready is the steady
state. failed surfaces in the UI as a retry affordance.
True iff a servable col thumbnail variant exists for this
asset AND the caller passes the content plane
(visibility.CanReadContent, ADR 0064). The client renders the
thumbnail only when this is true and otherwise shows a
placeholder WITHOUT requesting bytes — so a content-gated or
preview-less asset never fires a /variants/col (or /file)
request that would 404 and spam the console (#471). A
restricted asset the caller may not read reads identically to
“no preview” (false), so this never confirms restricted
(0064-safe).
Base64-encoded thumbhash (~30 bytes decoded) for instant blurred placeholder rendering on feed cards. Computed synchronously at upload for image assets. NULL for non-image resources.
Soft-delete timestamp. Non-null only on rows surfaced by
the admin include_deleted=true listing (the trash view);
null on live rows.
Optional reason captured at soft-delete time.
WebVTT subtitle / caption tracks attached to this asset. Empty array when none, or when the asset’s renderable kind doesn’t support subtitles (image, 3D, document, etc.). Tracks ride along with the asset; they do NOT appear in asset-count queries and do NOT federate as standalone activities.
One WebVTT subtitle track attached to an asset. NOT a first-class asset; rides along with its parent in the Asset.subtitle_tracks array. Tracks are uniquely identified by (asset_id, lang) — re-uploading the same lang replaces the existing row rather than creating a duplicate.
object
RFC 5646 / BCP 47 language tag (e.g. “en”, “en-US”, “ja”, “fr-CA”). The sentinel value “und” is reserved for sidecar files that matched by basename but had no inferrable language segment.
Optional human-readable label (“English (US)”, “Forced”, “Director’s commentary”). Empty string when not provided; never null.
CAS hash of the stored WebVTT file.
The format the track was uploaded in. The conversion worker stores WebVTT regardless; tracking the source lets operators re-convert if a converter bug surfaces.
1.0 for text-based sources (deterministic conversion). <1.0 for OCR’d bitmap sources like IDX (DVD subtitles). UI MAY surface a warning banner below 0.8.
Malformed request
object
Human-readable error summary
Example
the request could not be completedAuthentication required, missing, or invalid
object
Human-readable error summary
Example
the request could not be completedExample
{ "error": "authentication required: sign in and retry with a valid session or API token"}Asset with the same content hash already exists in the
user’s library and the operator’s
upload.dedup_behavior is block. Response carries
the existing asset id under existing_asset_id so
the UI can navigate there.
409 response shape returned
from POST /assets when upload.dedup_behavior is block
and a duplicate hash was detected. The caller is expected
to either navigate to the existing asset or change their
upload.
object
Human-readable error message.
The asset id the duplicate hash already maps to.
Unexpected server error
object
Human-readable error summary
Example
the request could not be completed