The calling user's home-page featured rail (server-ranked, at most 5).
const url = 'http://localhost:8080/v1/me/highlights?window_days=7';const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}use reqwest;
#[tokio::main]pub async fn main() { let url = "http://localhost:8080/v1/me/highlights";
let querystring = [ ("window_days", "7"), ];
let mut headers = reqwest::header::HeaderMap::new(); headers.insert("Authorization", "Bearer <token>".parse().unwrap());
let client = reqwest::Client::new(); let response = client.get(url) .query(&querystring) .headers(headers) .send() .await;
let results = response.unwrap() .json::<serde_json::Value>() .await .unwrap();
dbg!(results);}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.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Query Parameters
Section titled “Query Parameters”The lookback for the “most played” ranking. Does not affect “resume” or “recently added” reasons, which are unbounded and newest-first respectively.
Responses
Section titled “Responses”OK.
object
object
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.
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.
The live session, when reason is “live”. Null for every other reason.
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”.
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.
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.
Example
{ "items": [ { "reason": "live" } ]}Malformed or invalid request.
object
object
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.
Present on restart_required.
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.
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.
object
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.
object
object
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.
Present on restart_required.
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.
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.
object
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" } ] }}