Skip to content

The calling user's home-page featured rail (server-ranked, at most 5).

GET
/v1/me/highlights
curl --request GET \
--url 'http://localhost:8080/v1/me/highlights?window_days=7' \
--header 'Authorization: Bearer <token>'

The home landing page’s hero rail. Per-caller, resolved entirely from the bearer identity: it accepts no user_id and there is no admin variant.

THE SERVER OWNS THE RANKING. This was the explicit sign-off decision (2026-08-05): the rail’s order and its per-item reason are produced here, and a client renders what it is given. A client MUST NOT re-derive or re-sort the rail from /v1/apps and /v1/sessions — two rankers is precisely the drift this endpoint exists to prevent.

ENTITLEMENT-FILTERED, AND THIS IS NOT COSMETIC. The rail is derived from sessions, and a session row outlives the entitlement that authorised it — an admin revoking a user’s entitlement does not delete their history. Highlights therefore applies the SAME entitled + enabled predicate as GET /v1/apps (internal/crud/store.go entitledSQL). Without it, a revoked app reappears on the user’s home page with its play time attached. The launch itself is still refused at POST /v1/sessions — this endpoint is UX, not the authorization boundary — but surfacing a revoked title is an information leak in its own right.

BEST-EFFORT BY CONTRACT. items may be shorter than 5, and is empty for a user who has never launched anything. A client MUST render the page with an empty rail rather than treating it as an error.

window_days
integer
default: 7 >= 1 <= 90

The lookback for the “most played” ranking. Does not affect “resume” or “recently added” reasons, which are unbounded and newest-first respectively.

OK.

Media typeapplication/json
object
items
required
Array<object>
<= 5 items
object
app_id
required

The app this highlight is about. The client already holds the full AppListItem from GET /v1/apps and joins on this id — name, cover_url, hero_url, kind and favourite are DELIBERATELY NOT duplicated here. One source of truth for presentation; a second copy would drift the moment artwork is re-resolved.

string format: uuid
reason
required

WHY this app is on the rail. The server ranks and labels; the client renders the kicker and never re-derives the reason. Enumerated so a client can style each kicker (live = success, most_played / resume = accent, recently_added = info) without string-matching prose. Widening this enum is a later amendment; a client pinned to this version holds a closed union and must fall through to a neutral kicker for an unknown value rather than dropping the item.

string
Allowed values: live most_played resume recently_added
session_id
required

The live session, when reason is “live”. Null for every other reason.

string | null format: uuid
session_started_at
required

Sessions.started_at for the live session, so the client renders the elapsed clock itself and it ticks without polling. Null unless reason is “live”.

string | null format: date-time
play_seconds
required

Total play time in the requested window, clamped per-session (an unreconciled NULL ended_at is bounded, never open-ended) and excluding state=‘failed’. 0 when not applicable to the reason.

integer
last_played_at
required

MAX(ended_at) over the user’s non-failed sessions for this app. Null when the app has never been played to completion - which is the state of every app for a user whose only session is the currently-live one.

string | null format: date-time
Example
{
"items": [
{
"reason": "live"
}
]
}

Malformed or invalid request.

Media typeapplication/json
object
error
required
object
code
required

E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, restart_required, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.

string
message
required
string
live_sessions

Present on restart_required.

integer
session_id

Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.

string format: uuid
derived_tiles

Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.

Array<object>
object
id
required
string format: uuid
name
required
string
Examplegenerated
{
"error": {
"code": "example",
"message": "example",
"live_sessions": 1,
"session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"derived_tiles": [
{
"id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"name": "example"
}
]
}
}

Missing/invalid/expired/revoked token.

Media typeapplication/json
object
error
required
object
code
required

E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, restart_required, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.

string
message
required
string
live_sessions

Present on restart_required.

integer
session_id

Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.

string format: uuid
derived_tiles

Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.

Array<object>
object
id
required
string format: uuid
name
required
string
Examplegenerated
{
"error": {
"code": "example",
"message": "example",
"live_sessions": 1,
"session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"derived_tiles": [
{
"id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"name": "example"
}
]
}
}