Skip to content

Launch a session; returns signaling coordinates.

POST
/v1/sessions
curl --request POST \
--url http://localhost:8080/v1/sessions \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "app_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "profile_id": "example", "client_type": "browser", "mic": true, "stream": { "width": 1, "height": 1, "fps": 1, "bitrate_kbps": 1, "h264_profile": "constrained-baseline", "codec": "h264" } }'

UI-P5 adds ONE NEW FAILURE MODE on this existing endpoint: a profile_id outside the app’s launchable allow-list is 409 profile_not_launchable_for_app, and no session row persists.

NO EXISTING SHAPE OR STATUS CODE CHANGES - 409 was already emitted here (profile overrides disabled, codec unsupported by host) and is already declared below. What is new is one error code, which is additive because Error.code is an open string with no enum: a client that does not know it falls through to its generic-409 branch. The new refusal is only reachable for an app an operator has explicitly restricted; every pre-UI-P5 app has an empty allow-list and is unaffected.

409 rather than 400 or 403, deliberately. The id is valid and resolves, so 400 validation_failed - this endpoint’s answer for an UNKNOWN profile_id - would make “no such profile” and “this app does not offer it” indistinguishable. And 403 forbidden is the ROLE gate in this contract (“valid token, insufficient role”, raised before any resource lookup); this refusal is per-app stream-quality curation and every caller gets the same answer. It sits with its two neighbours: profile_ineligible (409, the DEVICE refuses a valid profile) and conflict (409, the app’s force policy refuses an override).

Its OWN code rather than the generic conflict, because conflict on this endpoint already carries two unrelated conditions (overrides disabled, codec unsupported by the placed host) and this is the only recoverable one: it means the caller’s menu is stale, so the remedy is to re-read GET /v1/me/profiles?app_id= and re-pick.

THE UI FILTER IS NOT THE ENFORCEMENT. GET /v1/me/profiles?app_id= returns the filtered menu, but this endpoint checks the allow-list itself, regardless of client - exactly as admin endpoints are gated server-side regardless of client. AN EXPLICIT stream OVERRIDE DOES NOT BYPASS IT: stream carries no role gate here, so honouring it would let any authenticated caller defeat the allow-list with one extra field. Only an admin bypasses it (role read server-side).

KNOWN GAP: POST /v1/sessions/{id}/swap does NOT re-resolve the profile, so a session launched on an unrestricted app keeps its wider stream when swapped into a restricted one. Deliberate - P2-07 swaps are no-resize and must not renegotiate. The allow-list is a launch-time rule.

STEAM LIBRARY DISCOVERY PHASE 2 ADDS A NEW TERMINAL STATUS TO THIS ENDPOINT - 403 forbidden - WHICH IS WHY THAT AMENDMENT IS NOT ADDITIVE. It is raised when the caller holds neither an ‘all’ nor a personal entitlement for app_id, and no session row persists. THIS IS THE AUTHORIZATION BOUNDARY OF THE WHOLE ENTITLEMENTS FEATURE; the entitlement-filtered GET /v1/apps is UX, and a client that ignores it and posts an app id directly is refused here.

403 rather than 404: GET /v1/apps/{id} answers 404 for a non-entitled app precisely because a READ can afford to say nothing. A launch cannot - the caller named this app deliberately, and “no such app” would send them to report a broken catalogue instead of asking an admin for access. 403 rather than 409: the three 409s around it (profile_ineligible, profile_not_launchable_for_app, quota) all mean “valid request, wrong right now”; this one is a statement about the CALLER, which is what 403 means in this contract.

CHECKED INSIDE THE SCHEDULING TRANSACTION, BEFORE THE QUOTA CHECK AND BEFORE PLACEMENT. Authorization precedes resource accounting - a caller who is both unentitled and at their session limit must be told “you may not launch this”, not “you have too many sessions” - and nothing is reserved for a launch that is going to be refused. It applies to EVERY ROLE, admin included; an admin who wants to launch an app they have restricted grants themselves the entitlement (POST /v1/admin/apps/{id}/entitlements), which is one call and leaves an audit row.

STEAM LIBRARY DISCOVERY PHASE 3 ADDS TWO 409 CONDITIONS, BOTH ADDITIVE (409 was already declared on this endpoint, and Error.code is an open string).

home_in_use is NOT a new code - P5-01 has emitted it since managed homes shipped. What Phase 3 changes is WHAT IT KEYS ON and WHAT IT CARRIES. The single-writer rule is per (user, HOME-OWNING app), and a derived tile does not own a home: it borrows its parent’s. So the guard resolves parent_app_id ?? id before counting, and a user with a Steam session live is refused on EVERY tile derived from it, and vice versa. IT IS ONE LOCK, held by the parent’s home, and the launcher tile is simply its most visible holder. Two Steam clients on one steamapps tree is a documented corruption class (a mid-write death marks the install unclean and forces a full re-verify), so this is correctness, not a limitation to engineer around. The body now carries session_id, the conflicting session. THE CLIENT MUST RENDER IT AS “Steam is already running - go to your session”, NAMING THE LAUNCHER TILE (what the user sees in their library) RATHER THAN A PARENT APP ID, and never as a generic launch failure: from the user’s side they clicked a game and were told a different app is in the way.

home_not_provisioned (409) IS a new code. A derived tile provisions nothing - the launch path calls RequireHome, which reads and never creates - so a user with no home for (user, parent) on any host is refused BEFORE placement, and NO user_homes ROW IS CREATED. The message names the parent app: “launch once on a host to create your library, then try again.” Deliberately stricter than the existing locality preference: a derived tile is pinned HARD to the host holding the home (via the existing PinHostID machinery) and cannot fall back to a free host, because a session that refuses to start is recoverable in one click while a session that silently creates a second empty Steam library is a support ticket and a wasted disk.

AND A THIRD, ADDED BY REVIEW RATHER THAN BY THE SPEC: 409 parent_app_disabled.

A TILE’S OWN enabled AND ITS PARENT’S enabled ARE ANDed AT LAUNCH. Launching a derived tile whose parent app is disabled is 409 parent_app_disabled, and the message NAMES the parent. This is a deliberate deviation from the implementation spec’s own field table, which lists enabled under “lives on the tile” and genuinely reads the other way. That table says which ROW a field is read FROM; it does not say the parent’s enabled is irrelevant. A tile has NO INDEPENDENT EXISTENCE - image, runtime, mounts and home are all the parent’s - so launching a tile IS running the parent, and enabled = false on an app in this system means “stop this from running”.

Without it that demonstrably did not hold: a reviewer verified empirically that a disabled parent’s tiles still launched, dispatching the disabled parent’s image against the disabled parent’s home, with no error and nothing in any UI warning the operator - because the tiles are separate rows and they still look enabled. An operator taking Steam out of service over a bad image tag, a mid-upgrade window or an incident would have had the inverse of a kill switch.

THE TILE IS NOT MODIFIED AND STILL APPEARS IN THE CALLER’S LIBRARY. Its own enabled is untouched and GET /v1/apps still lists it, so this is not derivable from the app object and a client cannot pre-empt it. That asymmetry is why it gets ITS OWN CODE rather than the generic conflict, on the same reasoning as home_not_provisioned: the remedy is an action on a DIFFERENT app by SOMEONE ELSE (an operator re-enabling the parent), and a client cannot derive that from a message string it is not allowed to parse. It is not the caller’s fault and there is nothing they can do about it.

THE PARENT’S NAME IS IN THE MESSAGE, NOT IN A STRUCTURED FIELD, and unlike home_in_use’s session_id that is deliberate: session_id exists because a client turns it into a link and must branch on its presence, whereas this has no client action attached - there is nothing to navigate to. A field nobody consumes is contract surface with no purchase. The message is human-readable and forwardable (“ask an operator to re-enable it”); it is NOT a machine-readable channel and must not be parsed.

Media typeapplication/json
object
app_id
required
string format: uuid
profile_id

User-facing selector (AS10-03).

string
client_type

Absent ⇒ browser.

string
Allowed values: browser native
mic

Microphone capture request (2026-08-02). Absent ⇒ false. Granted only when instance setting mic_capture_enabled is on; an ungranted request is not an error — the session stream body reports the granted state.

boolean
stream

Admin/debug/back-compat override; beats profile_id field-by-field.

object
width
integer
height
integer
fps
integer
bitrate_kbps
integer
h264_profile
string
Allowed values: constrained-baseline baseline main high
codec

Multi-codec: optional admin/diagnostic codec override. Orthogonal to the resolution envelope (a codec-only override does not bypass the eligibility gate). Bypasses the device-decode and failure-history clamps (forced re-test path) but not the host-encoder clamp (409 conflict if the placed host cannot encode it).

string
Allowed values: h264 h265 av1

Session assigned.

Media typeapplication/json
object
session
required
object
id
required
string format: uuid
user_id
required
string format: uuid
app_id
required
string format: uuid
host_id
required
string | null format: uuid
state
required
string
Allowed values: pending assigned starting running stopping stopped failed
state_detail
required
string | null
error_message
required
string | null
failure_code
required

First-run-experience §S5. Machine-readable classification of a terminal failure - “app_exited_early” today. Sits beside error_message (free-text prose that may be rewritten freely); the UI branches on THIS. Always serialized; null unless a failure warranted it.

string | null
app_log_tail
required

First-run-experience §S5. The app container’s own captured log tail (newline-joined, oldest first, ~100 lines bound) - the only surviving copy, since app containers run –rm. Always serialized; null unless a failure warranted capturing it. Rendered preformatted, distinct from error_message’s prose rendering.

string | null
profile_id
required

The profile the session was launched from; null for a legacy/tier/override launch. UI-P4: this is now a LAUNCH PROFILE id, i.e. the USER’S PICK. The rung it resolved to is stream_profile_id.

string | null
stream_profile_id
required

UI-P4: the RUNG this launch resolved to (e.g. “1080p60-h264”). Always serialized; null for every pre-UI-P4 session and for any legacy/tier/override/console launch. profile_id answers “what did the user pick”, this answers “what did they get” - and because a rung carries its own resolution, the two can legitimately disagree about width/height/fps/ bitrate. The stream block below is always the truth for the running session.

string | null
stream
required
object
width
required
integer
height
required
integer
fps
required
integer
bitrate_kbps
required
integer
h264_profile
string
Allowed values: constrained-baseline baseline main high
codec

Multi-codec: the resolved session video codec on session.stream; absent on an app’s display_stream. Additive; h264 for every pre-multi-codec session. h264_profile applies only when codec is h264.

string
Allowed values: h264 h265 av1
playout0_ms

AS-02: tier-selected initial receiver playout target (ms) on session.stream; absent on an app’s display_stream.

integer
mic

Microphone capture (2026-08-02): the GRANTED state on session.stream (request mic AND instance mic_capture_enabled); absent on an app’s display_stream and on pre-amendment sessions (absent = false).

boolean
external_width

Session-display-stream (approved 2026-08-16) (2026-08-16, approved — PR #15). Current EXTERNAL (encoded/streamed) width on session.stream; absent on an app’s display_stream. PRESENT WHENEVER THE CONTROL PLANE KNOWS the current external size — i.e. it has seen a 202 from PATCH /v1/sessions/{id}/display or a session_metrics sample reporting it — INCLUDING when that size equals the launch width/height. ABSENT MEANS UNKNOWN (a fresh control plane, or no such signal yet for this session), NOT “at launch size” — do not infer launch size from absence. Ephemeral, in-memory control-plane cache of the last-known value (agent session_metrics is the authoritative source), lost on a control-plane restart until the next 202 or session_metrics sample repopulates it. See control-api.md for the INTERNAL-vs-EXTERNAL vocabulary.

integer
external_height

Session-display-stream (approved 2026-08-16). Pairs with external_width; see its description.

integer
external_resize_supported

Session-display-stream (approved 2026-08-16). Whether the assigned host’s encoder can live-resize the stream at all (readback of agent-api.md session_metrics.external_resize_supported). Absent until an amendment-aware agent reports = unknown, never false.

boolean
external_owner

Abr-resolution-fps-ladder (approved 2026-08-16) amendment (2026-08-16, approved — PR #15). Who currently owns the live external size on session.stream: “auto” (the host’s ABR resolution ladder) or “pinned” (a PATCH /v1/sessions/{id}/display stream_width/ stream_height set it to a non-launch size, which suspends the ladder for the rest of the session or until released). Readback of agent-api.md session_metrics.external_owner. PRESENT ONLY WHEN KNOWN AND external_width/ external_height differ from the launch width/height — mirrors external_width’s presence rule, since the agent reports external_owner only in that same window. Absent on every pre-amendment session and while the external size sits at launch.

string
Allowed values: auto pinned
rungs

Session-display-stream (approved 2026-08-16). The fixed, aspect-ratio-filtered table of [width,height] pairs this session’s stream_width/stream_height may be set to via PATCH /v1/sessions/{id}/display (always <= the launch size, launch size always included). Always present on session.stream for a running session; absent on an app’s display_stream. Not the admin-configured stream-profile “rungs” (AS10-01) — a separate, fixed table unrelated to the admin encode-rung catalog. 21:9 family membership is by a set of reduced ratios {43:18, 64:27, 7:3} — 3440x1440 reduces to 43:18, 2560x1080 to 64:27 — control-api.md’s table is the reference, not a computed tolerance.

Array<Array<integer>>
codec_decision
required
One of:

UI-P6: how this session’s rung/codec was resolved. Persisted at launch and echoed on every session body; null for every pre-UI-P6 session and for any launch that walked no rung chain (console, or a legacy/tier launch that resolved no launch profile). THE THREE OUTCOMES ARE DELIBERATELY DISTINGUISHABLE and a client must not collapse them, because stream.codec is identical in all three. Won on merit: selected with rejected_by=null, clamps_bypassed=false, floor=false, override=null. Operator override: override names the forced codec and the selected rung has clamps_bypassed=true with rejected_by=null (it SKIPPED clamps 2/3, 4, 5 and 6 rather than surviving them). Floor: floor=true and the selected rung is clamps_bypassed=true WHILE STILL CARRYING the rejected_by that killed it - it was dispatched despite being rejected. floor and clamps_bypassed answer different questions (“did anything survive?” vs “was THIS rung measured?”) and the override case sets the second without the first; do not merge them.

object
result_rung
required

The dispatched rung id.

string
result_codec
required

Multi-codec: the WIRE session video codec (h265 is HEVC). Resolved server-side at launch; the client answers the single codec the host offers. Distinct from CatalogCodec, which spells HEVC ‘hevc’.

string
Allowed values: h264 h265 av1
override
required
One of:

Multi-codec: the WIRE session video codec (h265 is HEVC). Resolved server-side at launch; the client answers the single codec the host offers. Distinct from CatalogCodec, which spells HEVC ‘hevc’.

string
Allowed values: h264 h265 av1
floor
required

NO rung survived the clamp chain and the unconditional h264 floor fired.

boolean
considered
required

Every rung WALKED, in position order. The walk stops at the first survivor, so a clean top-rung win lists exactly one entry - that is the whole decision, not a truncation.

Array<object>

UI-P6: one rung’s verdict in the resolution walk.

object
rung_id
required
string
codec
required

Wire vocabulary (h264|h265|av1), except when rejected_by is unknown_codec, where it is the raw unmappable catalog value - the only useful thing to show for a hand-edited row.

string
rejected_by
required
One of:

UI-P6: the clamp that rejected a rung during the walk. Treat this set as OPEN - render an unrecognised value rather than assuming the list is closed. host_encoder=clamp 1, client_decode=clamp 2/3 (codec), decode_height=clamp 2/3 (resolution), decode_history=clamp 4, hardware_encoder=clamp 5, encoder_throughput=clamp 6 (#506: the host’s reported sustained encode throughput for this codec cannot carry the LAUNCH-EFFECTIVE width x height x fps - the rung’s values with any explicit stream.* size override applied - see agent-api.md capacity.codec_throughput; a host that reports no hint never rejects this way), unknown_codec=a rung whose catalog codec does not map (hand-edited data; codec then carries the raw catalog value).

string
Allowed values: host_encoder client_decode decode_height decode_history hardware_encoder encoder_throughput unknown_codec
selected
required

The rung that was actually dispatched. Exactly one entry per recorded decision.

boolean
clamps_bypassed
required

This rung was dispatched WITHOUT being measured against the full clamp chain - the clamp-0 override path (clamps 2/3/4/5/6 skipped) or the floor (every clamp skipped). Never set on a rung that won the walk.

boolean
negotiated_codec
required

UI-P6: the wire codec the CLIENT reports it is actually decoding (normalised getStats mimeType), beside stream.codec, which is what the SERVER resolved. They should agree; when they do not, that is how a silent fallback or a mis-negotiated m-line presents, so both are kept and neither is reconciled away. NOT constrained to the Codec enum - a value the server never resolves (vp9) is preserved rather than dropped, because it is the loudest disagreement there is. Always serialized; null until the client reports one.

string | null
health_state

AS10-06: computed stream-health classification; present only when the session is running/unsustainable.

string
Allowed values: healthy network_degrading abr_at_floor client_decode_degrading client_presentation_degrading unsustainable failed
health_reason

AS10-06: optional human explanation accompanying a degraded health state.

string
app_launch_state

Steam game-exit lifecycle amendment (2026-08-02): coarse in-container application launch state last reported by the agent (agent-api.md session_metrics. app_launch_state, the normative definition). Read-only; there is no request field. OMITTED when the app image does not report launch state - absence means UNKNOWN, and a client must treat an absent field and an unrecognised value identically and fall back to transport-level readiness. A hint and a metric, never a session-state authority and never access control: state remains the only progress signal.

string
Allowed values: starting client_only game_running game_exited
created_at
required
string format: date-time
started_at
required
string | null format: date-time
ended_at
required
string | null format: date-time
signaling
required
object
url
required

Wss:///v1/signal

string
token
required

Single-use, short-TTL, plaintext this response only.

string
expires_at
required
string format: date-time
ice_servers

#509. STUN/TURN servers the client configures its RTCPeerConnection with, sourced from control-plane configuration. Always serialized by the control plane (empty array when nothing is configured, which is the default and reproduces the pre-#509 hardcoded empty list). Declared optional rather than required so a client generated at this pin still parses a response from an older control plane; treat a missing key as an empty list.

Array<object>

#509. One ICE server, shaped as the W3C RTCIceServer dictionary so a browser client passes it through untranslated.

object
urls
required

One or more ICE URLs. Every entry carries a stun:, stuns:, turn:, or turns: scheme.

Array<string>
>= 1 items
username

TURN username. Present for turn:/turns: entries, absent for stun:/stuns: (STUN has no authentication).

string
credential

TURN credential. Present for turn:/turns: entries. Readable by every authenticated user who launches a session, which is inherent to browser TURN — deployments should issue ephemeral, time-limited credentials rather than a shared static password.

string
Example
{
"session": {
"state": "pending",
"stream": {
"h264_profile": "constrained-baseline",
"codec": "h264",
"external_owner": "auto"
},
"codec_decision": {
"result_codec": "h264",
"override": "h264",
"considered": [
{
"rejected_by": "host_encoder"
}
]
},
"health_state": "healthy",
"app_launch_state": "starting"
}
}

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"
}
]
}
}

Phase 2, NEW TERMINAL STATUS ON THIS ENDPOINT: the caller holds no entitlement for app_id. No session row persists.

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"
}
]
}
}

No such resource.

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"
}
]
}
}

Session_quota_exceeded / home_in_use / profile_ineligible / profile_not_launchable_for_app / conflict (pre-existing), or (Phase 3) home_not_provisioned - a derived tile whose parent has no home on any host - or parent_app_disabled.

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"
}
]
}
}

No_host_available / capacity_exhausted — well-formed but no room to place now (retryable).

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"
}
]
}
}