Skip to content

getAsset

GET
/assets/{id}
id
required
string format: uuid

The asset

WITHHELD ASSETS (#899). An asset you cannot OPEN must not hand you its metadata. When the caller fails visibility.FieldsReadable — the conjunction of the row plane and the ADR 0064 content plane — this object degrades to a placeholder: restricted flips to true and the ONLY other keys present are id and owner_display_name.

The placeholder is deliberately PRESENT rather than 404. ADR 0064 holds that sensitivity gates content, not rows, so the row stays visible; a reader is meant to be able to tell that something is there they cannot see, because that is what makes “request access” (#881) mean anything.

ALLOW-LIST, not deny-list. The permitted key set on a withheld asset is exactly id, restricted, owner_display_name — an identifier the caller already holds, a marker, and the one asset-derived value the owner permitted (“the placeholder should never leak info. Not even title. Only the owner’s name.”). Everything else is ABSENT, not blanked, so a client cannot tell “withheld” from “genuinely empty” and infer from the difference. A field added to this schema later is therefore withheld by construction rather than by remembering to add it to a list of known-sensitive names.

That key set is the SAME one PostMember’s placeholder carries, and deliberately so: the field set on this surface must never be wider than the field set for the same asset reached through a container, or membership becomes the narrower path and the three surfaces drift apart again.

WHY required IS SHORT. It used to name sixteen fields, on the #595 reasoning that field?: T in the frontend types let a surface hand-map a narrower object and silently drop the three CardThumb needs. That reasoning still holds for a readable asset — and it is exactly why absence has to be expressible for an unreadable one. A contract that DEMANDS title and file_hash cannot express a payload that withholds them. So required is now the two keys every asset payload carries at every tier, and the client branches on restricted (which is required, and therefore always present to branch on) before reading anything else.

object
id
required
string format: uuid
restricted
required

True when the caller may not see this asset’s columns. Every other key except id and owner_display_name is then absent, and the client renders a placeholder tile.

REQUIRED, so it is always present to branch on: an optional marker would read as undefined on a withheld payload from an older server and fall through to “not restricted”, which is the wrong default for a security marker.

boolean
owner_display_name

Set only when restricted is true: the display name of the asset’s owner, so the placeholder can say whose work it is and #881 can address the request. Absent — not empty — when the owner has no resolvable name or the asset is unowned, so a client cannot tell “withheld” from “genuinely empty”.

Not carried on a readable asset, which has owner_user_ref and can resolve the profile itself. owner_user_ref is NOT carried on a withheld one: the placeholder’s allow-list is the owner’s NAME, and a ref is a second way to ask.

string
title
string
description
string
asset_type
integer format: int64
owner_user_ref
integer format: int64
nullable
status
string
Allowed values: draft active archived
mature

This asset carries a MATURE rating (#1115, epic #1114, ADR 0090).

⚠️ A RATING IS NOT A CLEARANCE. sensitivity answers who is ALLOWED to see this; mature answers who has OPTED IN. They are orthogonal — a public artwork can be mature, and a restricted one need not be — and a client that treats one as a level of the other will get both wrong.

Whether a disqualified viewer receives this row at all, and whether its preview is blurred, are the server’s decisions (ADR 0090 §3). The flag is here so a client can LABEL what it was given, never so it can decide what to hide.

boolean
ai_provenance

The MAKER’S DECLARATION about generative-AI involvement in this work (#1167, ADR 0094).

⚠️ ABSENT/NULL MEANS UNDECLARED — NOBODY WAS ASKED. It does NOT mean none. Every asset that predates the feature is undeclared and was deliberately not backfilled: writing none onto those rows would assert “the maker declares no generative AI” on the maker’s behalf, which is a fabricated disclosure on the one topic where a false disclaimer is the worst available error. A client that renders a missing value as “no AI” is lying for the artist. Render it as “not stated”, or render nothing.

⚠️ A DECLARATION IS NOT A PERMISSION (ADR 0094 §4). This is orthogonal to sensitivity and to mature. It is a FILTER a viewer may apply to their own feed and NEVER a gate that withholds the work from anyone: the work stays public, findable and countable, and nothing derived from it — search text, facets, suggest, thumbhash, embeddings, counts, covers — is withheld on account of it. That is what keeps this field cheap, and it holds only while nothing gates on it.

Does not federate yet: the v1 envelope rejects unknown top-level fields. ADR 0094 §6 pre-decides the wire mapping for the next @context bump.

string
nullable
Allowed values: none assisted generated
file_hash

Sha256 of the linked storage object

string
nullable
file_extension
string
nullable
file_size_bytes
integer format: int64
nullable
tags

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.
Array<string>
tag_details

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.

Array<object>

One tag with full source/confidence/provenance metadata. Backs the per-source UI badge.

object
value
required

The tag value itself (e.g. “kittens”).

string
source
required

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.

string
Allowed values: manual ai import
confidence

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).

number format: double
nullable
created_by_provider

Provider slug (openai / claude / ollama) for source=‘ai’.

string
nullable
created_by_model

Model identifier (gpt-4o / claude-3-5-sonnet) for source=‘ai’.

string
nullable
metadata

Free-form JSONB metadata blob (EXIF, custom fields, etc.).

object
key
additional properties
any
state_id

Optional workflow state (domain = ‘asset’).

string format: uuid
nullable
team_id

The team this asset is scoped to, or absent when it has none — which is the common case. Set at create time via AssetCreate.team_id (#953) and not changeable afterwards.

It is on the FIELD plane, so a withheld asset does not carry it: which studio holds a file you may not open is a fact about that file.

string format: uuid
nullable
processing_status

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.

string
Allowed values: pending ready failed
preview_available

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).

boolean
ladder_available

True iff EVERY variant in the operator’s CONFIGURED preview ladder exists for this asset AND the caller passes the content plane. Same ADR 0064 contract as preview_available: a restricted asset reports false rather than 403, so the flag never confirms restricted.

This is the signal a responsive srcset needs. preview_available promises only col — a 320x320 COVER crop — so a client that inferred the wider preview / screen / hires rungs from it would request bytes that may not exist, which is exactly the 404 class #471 removed. With no way to ask, the frontend disabled responsive images entirely and every card serves a 320px square regardless of viewport. This flag is how the client stops guessing.

THE LADDER IS OPERATOR-CONFIGURABLE, so this is NOT “the four default rungs exist”. It is computed against sysconfig.GetPreviews, whose defaults (col/preview/screen/ hires) are a default and not the contract. An install that drops or adds a rung changes what this flag means, which is the intent — clients should read the ladder from GET /previews rather than assuming the default keys.

Deliberately NOT an alias for preview_available. On a healthy install the raster pass writes the whole ladder or none of it, so today the two are nearly co-extensive; they diverge exactly where it matters — assets whose raster pass partly failed, and any future partial-ladder state.

boolean
scrub_available

True iff a servable sprites.vtt cue file exists for this asset AND the caller passes the content plane. Same ADR 0064 contract as preview_available — a restricted asset reports false rather than 403.

This is the hover-scrub gate (#835). The card’s scrub used to be licensed by FILE EXTENSION — “this is an mp4, therefore it has a sheet” — which is a guess about storage made from a filename. It is wrong in both directions: a video whose expensive preview.video job has not drained yet has a col (the cheap poster job, #818) and no sheet, and the card requested one anyway and 404’d; and a format that DOES have a sheet but was not on the hardcoded list — an animated GIF since #832 — could never scrub no matter what was in storage.

sprites.vtt and not sprites.jpg because the cue file is what the client actually reads: it declares how many cells the sheet really contains, which is not cols×rows for a clip too short to fill the grid. Both are written together by every producer (preview.video, preview.3d, preview.gif), so either would answer “is there a sheet”, but only one answers “can I drive from it”.

boolean
anonymously_visible

True iff an ANONYMOUS visitor’s read rule would return this asset (#1209). Derived from the anonymous arm of visibility.Predicate for EntityAsset, which is soft-delete plus status = 'active' plus sensitivity = 'public' plus processing_status = 'ready' (ADR 0063; ADR 0020 governs the tiers).

NOT A STATEMENT ABOUT THIS CALLER, unlike every other flag beside it. preview_available, ladder_available and scrub_available answer “may YOU render this”; this answers “would a STRANGER be shown it”, and it has one answer per row regardless of who asks. A surface that reached for it as a readability decision would be asking the wrong question, and would be wrong for every signed-in caller.

WHAT IT IS FOR. A curator choosing a cover for a public collection or post is choosing a picture strangers will be shown, and a pick that fails this rule falls back on every anonymous surface with no explanation. The picker could previously check only status, because this schema deliberately carries no sensitivity, so a team-tier pick fell back unwarned. It said so rather than guessing, since a warning that guesses in the reassuring direction is worse than none.

IT DISCLOSES NOTHING NEW. The fact is derived from the GATE rather than from content, and it is exactly what any visitor can establish by opening the same URL with no session. It rides only on payloads that already passed the field plane: the withheld-asset placeholder is a complete literal carrying id, restricted and owner_display_name, and does not carry this, so a withheld row states nothing about its own tier.

Absent on a withheld row, present everywhere else.

boolean
thumbhash

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.

string
nullable
pixel_width

Recorded pixel width of the SOURCE image, or null when the install has never measured one.

REQUIRED-but-nullable for the same reason as thumbhash above: it is a field the card frame reads to decide what a tile IS. Masonry sizes each tile from this ratio so the space is reserved before the image loads and the wall does not reflow as 72 images arrive (#640).

Not a column on assets — the value lives in asset_field_value under the pixel_width field definition seeded for #618, and is projected here.

The quantity is the shape of the image the preview ladder’s contain rungs are built from, NOT the source file’s pixels (ADR 0071 §6). For a raster the two coincide with one deliberate refinement: the ladder source is the EXIF-ROTATED image, so an orientation=6 phone photo reports the portrait shape a viewer sees rather than the landscape grid stored on disk. For everything with no source pixels at all — a 3D model, a font, an audio file, a plain-text document — it is the shape of the one image that kind produces on its way through the pipeline: a turntable frame, a glyph specimen, a waveform, a rendered plate. A 2048x384 waveform is a 5.33:1 tile and nothing in the .ogg says so.

Null therefore means only “no preview render has recorded a shape for this asset yet” — a draft (the render selects status = 'active'), a kind no preview handler can render at all, or an asset whose preview predates #757 and has not been re-rendered. The client falls back to measuring the image once it loads.

integer format: int32
nullable
pixel_height

Recorded pixel height of the source image, or null. See pixel_width — the two are projected as a PAIR, so a consumer never has to handle one without the other.

integer format: int32
nullable
created_at
string format: date-time
updated_at
string format: date-time
deleted_at

Soft-delete timestamp. Non-null only on rows surfaced by the admin include_deleted=true listing (the trash view); null on live rows.

string format: date-time
nullable
deleted_reason

Optional reason captured at soft-delete time.

string
nullable
subtitle_tracks

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.

Array<object>

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
lang
required

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.

string
label

Optional human-readable label (“English (US)”, “Forced”, “Director’s commentary”). Empty string when not provided; never null.

string
file_hash
required

CAS hash of the stored WebVTT file.

string
source_format
required

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.

string
Allowed values: vtt srt ssa ass sub idx
confidence
required

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.

number format: float
<= 1
created_at
string format: date-time
card_fields

The values of the fields an operator marked show_on_card, already resolved to display strings and in the order the field definitions declare (#552).

Absent or empty when nothing is marked, when this asset carries no value for any of them, or on a restricted placeholder — a card MUST fall back to its own default rather than render an empty strip. That fallback is what makes the flag a hint: a client that never reads this key is plainer, not wrong.

Only ungated fields can appear, structurally: the schema refuses show_on_card on a field carrying a read_capability, so this array cannot become a side door around a per-field gate.

Array<object>

One at-a-glance field value on a card (#552). Deliberately a DISPLAY shape and not a second AssetFieldValue: the card shows one line per field, so the server resolves the vocabulary slug to its label here rather than shipping the slug plus a resolution map to every tile on a browse page.

object
code
required

The field definition’s federation-stable slug.

string
label
required

The operator-facing name, for the caption.

string
value
required

The value as one line. Never empty — a field with nothing on this asset is omitted from the array rather than sent blank.

string
origin

The peer this asset came from, or absent/null when it is ours. Present so a surface can answer “whose is this?” without making remote work look lesser — federated content uses the same card, the same viewer and the same display hints; what it must not do is arrive unattributed.

object
peer_id
required
string format: uuid
display_name
required
string
instance_url
string
owner

The renderable identity behind owner_user_ref — the artist block on an asset card (#1047).

THE SAME SHAPE AND THE SAME RESOLUTION AS A POST’S AUTHOR, because it is literally the same code: users.LookupAuthors, batched once per page. An asset card that resolved the owner client-side would be an N+1 per tile, and worse, it would re-derive the display-name ladder in the browser — which is how the anonymous rung gets skipped (#1023).

ABSENT MEANS “NOT DISCLOSED”, NOT “NO OWNER”. A ref is omitted from the lookup when the owner set hide_from_anonymous and the reader is anonymous (ADR 0024 / ADR 0070 §3), or when the account is gone. There is no redacted entry and no placeholder identity: the card draws no artist block at all. Deliberately not a sentinel — “someone who opted out owns this” still discloses that they own it.

Absent on a restricted placeholder too, which carries owner_display_name instead: that surface has its own allow-list and its own name rule (users.PlaceholderOwnerName), and a second, wider identity object riding the same row would defeat it.

object
ref
required
integer format: int64
username
required

The handle. Also the profile link target (/users/by-username/{username}).

string
display_name
required

Resolved display string ready to render — never empty.

RESOLUTION IS CALLER-DEPENDENT and matches UserPublic’s exactly, because it is literally the same expression (users.ResolveDisplayName):

authenticated: profile.display_name → user.fullname → username anonymous: profile.display_name → username

The anonymous rung skips fullname on purpose. Real name is authenticated-only (ADR 0070 §3), and a display-name fallback that reaches it would be that rule defeated by a COALESCE.

string
avatar_url

Null for users with no uploaded avatar; the card renders a generated initials disc in that case.

string
nullable

Authentication required, missing, or invalid

object
error
required

Human-readable error summary

string
Example
the request could not be completed
Example
{
"error": "authentication required: sign in and retry with a valid session or API token"
}

Resource not found

object
error
required

Human-readable error summary

string
Example
the request could not be completed