Choose or clear the team's hero picture
PUT /teams/{id}/hero
Points the team at an ordinary asset to use as its picture (#982), or clears the pointer so the team falls back to its initials tile.
Why this is not part of updateTeam
The gate is different. Renaming a team is a global teams.admin
act; choosing its picture is one a team’s OWN admin should be
able to do, so this endpoint accepts teams.admin scoped to
this team. Folding it into the PATCH would mean one operation
answering to two different authorisation rules depending on
which field the body happened to carry, which is how a gate ends
up being widened by accident.
What the server will accept
The asset must be sensitivity: public AND carry this team’s
team_id. Both halves are required: public alone would let a
team pin any public asset in the install onto itself, and
ownership alone would paint a team- or restricted-
sensitivity picture into a strip anonymous readers can see.
An asset that fails either half is refused with the same 400 given for an id that does not exist — telling the two apart would make this an existence oracle for assets the caller cannot see.
A just-uploaded asset is accepted before its col rendition
exists; the team simply keeps painting initials until the
rendition lands. Refusing it would be an error the admin cannot
act on.
Passing the check is not a promise the picture keeps rendering.
The read path re-checks the same rule on every read, so an asset
later set to restricted drops out of the payload and the team
reverts to initials.
Authorizations
Section titled “Authorizations ”Parameters
Section titled “ Parameters ”Path Parameters
Section titled “Path Parameters ”Request Body required
Section titled “Request Body required ”Choose or clear a team’s hero picture (#982). Exactly one of the two properties must be present — an empty body is a 400 rather than a no-op, because a request that says nothing about a single-valued endpoint is a client bug, not an instruction.
object
The asset to use as this team’s picture. Must be
sensitivity: public AND carry this team’s team_id; any
other asset — including one that does not exist — is
refused with the same 400.
Not nullable: sending null to mean “remove” is exactly the
shape that stops working the moment the Go field is a
pointer with omitempty, because absent and null collapse
before the handler sees them. Removal is clear_hero.
Remove the chosen picture, reverting the team to its
initials tile. Mutually exclusive with asset_id — sending
both is a 400, rather than the server picking one and
silently discarding the other.
Same shape, and the same reasoning, as clear_cover on
CollectionUpdate and clear_expires_at before it.
Responses
Section titled “ Responses ”The team, carrying its new hero (or none)
object
URL-friendly identifier, unique per origin server. Used in display and (eventually) federation actor URIs.
Example
aurora-rndExample
Aurora R&DSet on rows mirroring a federated team. NULL = locally owned.
The team’s picture (#982) — fetch it from
/assets/{hero_asset_id}/variants/col. NULL means the
client falls back to the team’s initials tile.
THIS IS THE RENDER ANSWER, NOT THE STORED SETTING. The
server re-derives it on every read, and it is absent
whenever the chosen asset no longer qualifies: set to
restricted, moved to another team, soft-deleted, or still
missing its col rendition. The stored pointer survives all
of those, so restoring the asset brings the picture back
without an admin re-picking it — but until then the wire
says “no hero” and the client paints initials.
One field rather than the setting/render pair Collection
carries, because the two cannot disagree PER READER here.
ADR 0088 gates a representative image per viewer; #982
narrows that for a team, because the followed-teams rail is
a navigation strip and a strip that shows some teams’
pictures and not others depending on who is looking is noise
rather than security. A team hero is admissible only if it
is public AND owned by that team — visible to everyone or
to no one — so a single field cannot be misread.
It does NOT federate: a local asset id names something that exists only on this server.
Direct parent teams (single hop). Empty for root teams.
Closure-walked ancestors live behind the listTeams
ancestor filter; this field is just the edges out of
the current node.
object
Direct memberships. Present on listTeams — the directory
card shows it (#684) — and ABSENT from getTeam.
The asymmetry is deliberate and is about staleness, not
taste. getTeam reads through an LRU that only the
team-row and parent-edge endpoints invalidate; adding a
membership does not, and never needed to, because nothing
membership-shaped was cached. A count served from that
entry would go stale the first time somebody joined. The
list path is uncached, so it can carry the number honestly.
A team page that wants the count reads
GET /teams/{id}/members, which it already fetches for the
member strip.
Live (non-soft-deleted) assets carrying this team_id — the
directory card’s “how much is in here” hint (#684). Present
on listTeams only, for the same caching reason as
member_count.
NOT visibility-filtered, deliberately. It counts assets
whose fields the caller may not read. That discloses
nothing new: this operation needs teams.read, so the
caller is signed in, and the authenticated asset predicate
already hands them restricted rows as placeholders — the
same number is reachable by paging
/assets?team_id=X. Filtering it would make the card
disagree with the page it links to.
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 completed