Partial update of a collection
PATCH /collections/{id}
Only fields present in the body are touched; omit a field to
keep its current value. Send clear_expires_at: true to remove
the TTL — sending expires_at: null does NOT, because null is
already “leave alone” for every property here.
Optimistic concurrency: pass the collection’s
last-known updated_at as if_unchanged_since; 409 if
the row has been edited since.
Authorizations
Section titled “Authorizations ”Parameters
Section titled “ Parameters ”Path Parameters
Section titled “Path Parameters ”Request Body required
Section titled “Request Body required ”Partial update; only present fields are touched. A null is
“leave alone”, not “clear” — removing a value that is already
set is an explicit flag (clear_expires_at, clear_cover,
clear_featured_cover, clear_featured_cover_focal,
clear_cover_focal, clear_featured_cover_zoom,
clear_cover_zoom).
object
Set this collection’s expiry. Send clear_expires_at: true
to remove an existing TTL — sending null here does not
clear it, for the same reason it does not for
cover_asset_id: null already means “leave alone”.
Remove this collection’s TTL, so it no longer expires (#1073).
Mutually exclusive with expires_at — sending both is a 400,
rather than the server picking one and silently discarding
the other.
Choose this collection’s cover picture (#1027). Any asset you may PICTURE — it does not have to be a member of this collection.
The server refuses an asset you may not picture, with the same 400 it gives for an id that does not exist: telling the two apart would make this endpoint an existence oracle for assets you cannot see.
CHANGING IT DISCARDS THE OLD FRAMING (#1333). A focal
point and a zoom are chosen against one particular picture,
so they mean nothing on the next one; swap the cover without
saying anything about framing and cover_focal_x,
cover_focal_y and cover_zoom all go back to null. Send
the new framing in the SAME request to keep it, which is
what the cover editor does. Re-sending the id already
stored is not a change and disturbs nothing.
Send clear_cover: true to go back to the derived mosaic —
a partial update cannot express “remove” by sending null,
because null is already “leave alone” for every other
property here. Same reasoning, and the same shape, as
clear_default on FieldDefinitionUpdate.
EDIT-ONLY, deliberately absent from CollectionCreate. #914 took a question off the new-collection dialog because the API already had a sensible default; a cover is a curation act performed on a collection that already has members, not a decision anyone can make while naming an empty one.
Remove the chosen cover, reverting this collection to the
derived mosaic composed from its members. Mutually exclusive
with cover_asset_id — sending both is a 400, rather than
the server picking one and silently discarding the other.
Choose this collection’s FEATURED-RAIL cover (#1207). Any
asset you may PICTURE, on exactly the terms cover_asset_id
states — including the same deliberately-indistinct 400 for
“no such asset” and “not yours to look at”.
Changing it discards the FEATURED slot’s framing on the
same terms cover_asset_id describes (#1333):
featured_cover_focal_x, featured_cover_focal_y and
featured_cover_zoom go back to null unless the request
supplies new ones. The two slots are independent, so this
never disturbs how the collection card is framed.
Send clear_featured_cover: true to go back to “no separate
choice”, which puts the rail back on cover_asset_id and
then on the derived cover. Null here is “leave alone”, as it
is for every other property on this schema.
Remove the separate featured-rail cover, so the rail falls
back to cover_asset_id and then to the derived cover
(#1207). Mutually exclusive with featured_cover_asset_id —
sending both is a 400.
It does NOT touch the focal point. The two are independent
settings: the focal point positions whichever picture the
rail ends up showing, so clearing the separate cover leaves
the positioning applying to the regular one. Clear the
positioning with clear_featured_cover_focal.
Centre the featured rail’s 890:500 crop horizontally, as a fraction of the picture’s width (#1207). 0 is the left edge, 1 the right, and out-of-range is a 400 rather than a silently-clamped value.
MUST BE SENT WITH featured_cover_focal_y. A focal point is
a point; half of one is not a weaker positioning but an
unanswerable one, so sending one without the other is a 400
rather than the server inventing the missing axis.
The vertical half of the focal point — 0 is the top edge, 1
the bottom (#1207). Must be sent with
featured_cover_focal_x; see there.
Put the featured crop back to centred (#1207) — one flag for the PAIR, because “remove the positioning” is a single intention over two columns and a per-axis flag would let a caller express half a clear.
Mutually exclusive with either focal coordinate: sending
both is a 400. Independent of clear_featured_cover, which
removes the picture rather than its positioning.
Centre the COLLECTION cover’s 4:3 crop horizontally, as a
fraction of the picture’s width (#1207; the destination is
4:3 and not a square, see the read schema and #1334). Same
rules as the featured pair: must be sent with
cover_focal_y, must be in 0..1, and out-of-range is a 400
rather than a silently clamped value.
Send it in the SAME PATCH as a new cover_asset_id to keep
it: a cover swap with no framing supplied clears the pair
(#1333).
The vertical half of the collection cover’s focal point.
Must be sent with cover_focal_x; see there.
Put the collection cover’s 4:3 crop back to centred
(#1207), one flag for the PAIR, for the reason
clear_featured_cover_focal is one flag for its pair.
Mutually exclusive with either coordinate. Independent of
clear_cover, which removes the picture rather than its
positioning.
Tighten the featured rail’s crop to this multiple of the fitting rectangle (#1212). 1 is the fit itself; 2 shows a quarter of the area.
INDEPENDENT OF THE FOCAL PAIR, and sent on its own. “Zoomed
in, still centred” and “at the fit, positioned left” are
both ordinary framings, so zoom is not validated with
featured_cover_focal_x/_y and does not share their clear
flag.
Refused outside 1..4 with a 400 rather than reaching the
column CHECK as a constraint error. Send
clear_featured_cover_zoom to go back to the fit; omitting
this field leaves whatever is stored alone, which is why the
clear flag exists at all.
Remove the featured cover’s zoom, returning it to the fit
(#1212). Mutually exclusive with featured_cover_zoom — 400
if both are sent, the exclusivity rule every clear flag on
this endpoint carries.
Tighten the COLLECTION card’s crop (#1212). Same rules as
featured_cover_zoom, over the 4:3 tile.
Remove the collection cover’s zoom, returning it to the fit
(#1212). Mutually exclusive with cover_zoom.
Optimistic-concurrency guard. When set, the
server returns 409 EditConflict if the collection’s
updated_at has advanced past this value. Omit to
opt out of the check (legacy behaviour).
Responses
Section titled “ Responses ”Updated
A user-owned collection of assets, per ADR 0009. UUID-keyed,
federation-prepared (origin_server_id), with three
orthogonal axes: visibility, membership, and lifecycle.
object
The collection’s read tier.
public means anonymous visitors may read it when the
instance has public mode on. It has been in the column’s
CHECK constraint since migration 00008 and in the read rule
(visibility.Predicate over EntityCollection admits
visibility = 'public') ever since; only this schema, and
therefore every client, kept saying otherwise — the same
defect #1176 fixed for posts. A collection could be made
public by no route the API offered (#1195).
manual only for 1.11.A. query and hybrid ship in
1.11.B once the search DSL (ADR 0010) lands.
TTL. Null = permanent.
Free-form admin-set hint (“for_review”, “for_proofing”, …).
Replaces RS’s single-purpose archived flag.
Federation home server. Null = local. Populated when collections sync in from a peer in a later phase.
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.
The curator’s CHOSEN cover picture (#1027), or null to compose the derived mosaic from members instead.
ANY asset the curator could picture when they set it — not necessarily a member of this collection. That is what makes it survive the member being removed, which is the failure a member-only pointer has. “Upload a dedicated banner” is still available: upload it as an ordinary asset, then pick it. One extra step, no separate upload machinery, and one representation for a cover instead of two.
THIS IS THE CURATOR’S SETTING, NOT THE RENDER ANSWER. Read
covers to know what to paint; read this to populate the
edit form’s picker. They disagree on purpose whenever the
reader may not picture the chosen asset — see covers.
It does NOT federate. A local asset id names something that exists only on this server, which is ADR 0083’s exclusion criterion; a peer composes its own mosaic from the members it holds.
The curator’s cover for the FEATURED RAIL specifically (#1207), or null for “no separate choice”.
The rail’s card is locked to 890:500 while a collection card is 4:3, so the picture that reads well on one is often not the one that reads well on the other. This is the second pointer that lets them differ.
NULL IS A FALLBACK, NOT A BLANK. The rail’s preference order
is this cover, then cover_asset_id, then the derived
hero-card cover of ADR 0027 — and each rung is re-checked
against the reader’s own picture plane, so a chosen cover
the reader may not picture falls through to the next rung
rather than leaving a blank tile or leaking the withheld
picture.
Like cover_asset_id: any asset the curator may picture,
not necessarily a member; the curator’s SETTING rather than
the render answer; and not federated.
Where the featured rail’s 890:500 crop is centred horizontally, as a FRACTION of the picture’s width — 0 is the left edge, 1 the right (#1207).
A fraction rather than a pixel offset because the rail
renders whichever preview rung the ladder serves at whatever
width the viewport gives it. A fraction is correct at every
rung and every width; a pixel offset measured against the
original is correct at none of them. It maps straight onto
CSS object-position.
Null means centre — the CSS default and what every collection had before this existed. Null and an explicit 0.5 are stored differently on purpose: the editor’s reset is a clear, so “never positioned” stays distinguishable from “positioned dead centre”.
Always null or set TOGETHER with featured_cover_focal_y; a
focal point is a point, and the database constrains the pair.
The vertical half of the featured crop’s focal point — 0 is
the top edge, 1 the bottom (#1207). See
featured_cover_focal_x for why it is a fraction, why null
means centre, and why the two travel together.
Where the COLLECTION cover’s 4:3 crop is centred horizontally, as a fraction of the picture’s width (#1207).
THE DESTINATION SHAPE IS 4:3, NOT A SQUARE (#1334).
CollectionCard paints a chosen cover inside an
aspect-[4/3] tile on the hub, on a profile and in search,
and that tile is the only collection surface that crops this
picture. The square is the tempting wrong answer because the
col rendition IS one (fit: cover at 320px, a 320x320
centre-crop, what every small collection thumbnail is made
of), but col is a SOURCE and not a destination: a client
that positioned against it would show a region the card
never displays. A crop locks to the dimensions of the thing
that renders it.
A separate pair from featured_cover_focal_x because the
featured card is 890:500: the point that centres a face in a
wide band is not the point that centres it in a 4:3 tile, so
one fraction cannot be right for both destinations.
⚠️ IT IS CHOSEN AGAINST THE ORIGINAL PICTURE. A consumer
honours it by rendering a contain rung with
object-position; applying it to col crops a crop, since
col’s square was already taken at the centre before this
value could act.
Null means centre. Swapping or removing the cover picture without supplying new framing clears it (#1333), because a fraction chosen against one photograph means nothing on the next.
The vertical half of the collection cover’s focal point
(#1207). See cover_focal_x.
How far the featured rail’s 890:500 crop is tightened, as a multiplier on the fitting rectangle (#1212).
The crop window is the FIT window divided by this, so 1 is
the fit itself and 2 shows a quarter of the area. It exists
because the fit window always has one axis equal to the
whole picture — object-fit: cover keeps one axis whole —
so at the fit exactly one axis can travel, and a subject
sitting in the left half of a portrait cover can never be
brought to the middle. Below the fit BOTH axes gain travel,
which is what makes featured_cover_focal_x mean anything
for such a picture.
Null means fit: what every collection rendered before this existed, so a client that ignores it renders exactly what it rendered before. Null and an explicit 1 paint the same picture and are stored differently on purpose — the editor’s reset is a clear, so “never zoomed” stays distinguishable from “zoomed and came back”.
Bounded 1..4. Below 1 the window would be larger than the
picture. The upper bound comes from the preview ladder’s
real rungs: a cover carrying a crop is painted from a
CONTAIN rung, the rung it is guaranteed to have is preview
(1024) and the tallest one is hires (4096), so 4x is the
last step the ladder can still answer with real pixels.
How far the collection card’s 4:3 crop is tightened (#1212).
Separate from featured_cover_zoom for the reason the focal
pairs are separate: the tightening that frames a subject in
a wide band is not the one that frames it in a 4:3 tile. See
featured_cover_zoom for the semantics and the 1..4 bound.
What to PAINT for this collection’s tile — the curator’s chosen cover if there is a renderable one (#1027), else up to four members whose picture this caller may render, the derived mosaic (#1026).
A COVER THIS READER MAY NOT PICTURE FALLS BACK TO THE MOSAIC; it never yields a blank tile, and it never leaks the withheld picture either. The choice is the curator’s, but whether it renders is still decided per reader, so two readers of the same collection can correctly get different answers here. A client cannot tell an overridden cover from a one-member mosaic, and does not need to.
POSTS ARE THE ONLY SOURCE (#1236). Each contributing
member is a collection_posts row, supplying
cover_thumbnail_asset_id ?? cover_asset_id — the same
preference a feed card uses. #882 made a post-only
collection an ordinary thing to own (“save someone else’s
post”), and until #1026 such a collection had no cover
source at all and rendered as an empty folder.
collection_resources rows used to contribute their asset
directly, and stopped when #1161 removed bare-asset
membership as a publication path: a tile summarises what a
collection CONTAINS, and after #1185 took the asset section
off the page those assets were inside nothing a reader
could open. A COLLECTION WITH NO POSTS THEREFORE HAS NO
COVERS — the field is absent, and the client paints its
empty state.
ORDER is the curator’s, added_at ascending: the
arrangement is the curation.
EACH ASSET APPEARS AT MOST ONCE, at its earliest position.
Two posts in one collection can share a cover, and the same
picture twice summarises nothing. Clients may therefore key
a list on asset_id.
WITHHELD MEMBERS ARE SKIPPED, NOT SLOTTED. Only members
passing the caller’s picture plane (ADR 0064) and having a
col rendition appear, so a restricted member never
occupies a slot a renderable one behind it could fill. A
blank quarter of a mosaic says nothing and costs a picture.
ABSENT means “not composed on this surface”, never “no members” — an empty array is the honest “nothing to show”.
One tile of a collection’s composed mosaic cover (#1026). An
entry exists only if this caller may render the asset, so
clients paint every entry unconditionally — there is no
preview_available to branch on and no placeholder rung.
object
Fetch the picture from
/assets/{asset_id}/variants/col. Guaranteed to have that
rendition — an asset without one is not a cover candidate.
A CONTAIN rung (preview) exists for this asset (#1207) — a
different question from the col guarantee above, and the
one a focal point depends on.
⚠️ A COVER WITH A FOCAL POINT MUST BE PAINTED FROM A CONTAIN
RUNG. col is fit: cover at 320px, a 320x320 CENTRE-CROP,
so the picture’s edges are gone before object-position
could act on them: applying collections.cover_focal_* to
col crops a crop and lands somewhere the curator did not
choose. The focal fractions are stored against the ORIGINAL,
so the source has to be one that still is the original
shape.
When this is false there is no such rung and the client
falls back to col with a centred crop — which is exactly
what every collection card did before focal points existed,
so the fallback is the old behaviour rather than a
degradation.
Populated for every entry, mosaic tiles included, even though only the single-cover case consumes it: a field that is present on some rows and absent on others is a field every reader has to branch on twice.
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"}Authenticated but missing required capabilities
object
Human-readable error summary
Example
the request could not be completedResource not found
object
Human-readable error summary
Example
the request could not be completedEdited by someone else after if_unchanged_since.
Returned with HTTP 409 when an optimistic-concurrency check
fails. Carries the row’s current updated_at so the
client can decide whether to reload + retry or surface a
diff UI.
object
Human-readable summary (e.g. “asset was edited by someone else”).
The current updated_at on the row (server-authoritative).