UI-P4: edit a launch profile, including reordering its rungs (order IS preference).
const url = 'http://localhost:8080/v1/admin/launch-profiles/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0';const options = { method: 'PATCH', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"id":"example","display_name":"example","description":"example","visibility":"user","sort_order":1,"rungs":["example"]}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}use std::str::FromStr;use serde_json::json;use reqwest;
#[tokio::main]pub async fn main() { let url = "http://localhost:8080/v1/admin/launch-profiles/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0";
let payload = json!({ "id": "example", "display_name": "example", "description": "example", "visibility": "user", "sort_order": 1, "rungs": ("example") });
let mut headers = reqwest::header::HeaderMap::new(); headers.insert("Authorization", "Bearer <token>".parse().unwrap()); headers.insert("Content-Type", "application/json".parse().unwrap());
let client = reqwest::Client::new(); let response = client.request(reqwest::Method::from_str("PATCH").unwrap(), url) .headers(headers) .json(&payload) .send() .await;
let results = response.unwrap() .json::<serde_json::Value>() .await .unwrap();
dbg!(results);}curl --request PATCH \ --url http://localhost:8080/v1/admin/launch-profiles/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0 \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "id": "example", "display_name": "example", "description": "example", "visibility": "user", "sort_order": 1, "rungs": [ "example" ] }'Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Request Bodyrequired
Section titled “Request Bodyrequired”UI-P4 admin create/edit shape. THE H.264 FLOOR RULE - the rung list must contain at least one rung whose codec is h264, or the write is 400 validation_failed. “And it must be LAST” is a WARNING (h264_floor_not_last), not a rejection: rejecting would make a migrated launch profile whose stored codec order puts h264 first permanently uneditable, and would add no safety, because the guarantee is the resolver’s unconditional floor (if no rung survives the clamp chain, the last h264 rung dispatches bypassing EVERY clamp, including its own hardware_encoder_required).
object
Required on POST; ignored on PATCH.
ORDERED stream-profile ids, best first. ORDER IS PREFERENCE. A stream profile may appear at most once (a duplicate is 400 validation_failed). An id that does not resolve is 400 validation_failed at write time, never an FK error surfacing at launch.
Responses
Section titled “Responses”OK.
object
Debug/internal launch profiles are never returned by GET /v1/me/profiles.
object
Preference order, 1 = tried first. Server-assigned from the ordered id array on write; a client never sends positions.
ONE ENCODE RUNG (UI-P4): a single codec at a single resolution, frame rate and bitrate. NOT user-facing - a user picks a LaunchProfile, which lists these in preference order. BREAKING vs the pre-UI-P4 shape: codecs[] and its launchable|future|unsupported status enum are GONE, replaced by a single codec. A rung IS a codec, so there is nothing left for a status to describe; a codec is offered because a rung using it exists in a launch profile, and withdrawn by removing that rung.
object
Rungs created by migration 0036’s fan-out use ‘
The CATALOG codec vocabulary used by stream profiles (rungs). Note ‘hevc’, not the wire ‘h265’ - the rename is bridged in exactly one place server-side and never on this surface.
Meaningful only when codec is h264. The browser (WebRTC) receiver rejects High on both VA and NVENC, so a browser launch still negotiates down to constrained-baseline; this records the rung’s preference for a capable client.
Present only on a WRITE response (PATCH /v1/admin/stream-profiles/{id}), and omitted entirely when there is nothing to say, so every read path is byte-identical to before. Reuses UI-P4’s WriteWarning shape rather than inventing a second one. Emitted when a codec change leaves every launch profile listing this rung still carrying a launchable H.264 rung, but no longer LAST. That warns rather than rejects, because the 0036 fan-out preserved each profile’s stored codec order and H.264 is often first, so rejecting would make migrated profiles permanently uneditable. A change that would leave a chain with NO launchable H.264 rung is a 409 instead, naming the affected launch profiles: an operator editing one rung has no way to know which chains depend on it. This guard exists because the rule was previously enforced only when editing a launch profile’s rung LIST, so editing a stream profile’s codec could silently invalidate every chain listing it, with nothing surfacing until a launch failed at dispatch. See docs/design/plans/2026-07-29-library-ux-fixes-spec.md.
UI-P4: a non-fatal advisory returned on an admin profile write. A warning NEVER fails the request - it reports a configuration that is legal but almost certainly not what the operator meant.
object
H264_floor_not_last - every rung after the H.264 rung is unreachable, because H.264 passes every clamp. floor_not_least_demanding - the H.264 rung has a higher min_offer_bandwidth_kbps or min_decode_height than a rung above it, or requires a hardware encoder while a rung above it does not; a floor harder to satisfy than the rung above it is a misconfiguration.
Rungs created by the fan-out are ‘internal’: a rung is never offered standalone, only via the launch profile that lists it.
Admin read only: the launch profiles listing this rung. Shown inside the editor as well as the list, because editing a shared object changes every consumer.
A minimal reference to a profile object, used in ‘used by’ lists.
object
Admin read only, ADDITIVE, omitted when zero: how many session rows record this rung as the one they resolved to. This is the SECOND “used by” dimension and it also blocks DELETE - sessions.stream_profile_id is a plain foreign key with no ON DELETE clause (NO ACTION on purpose: ON DELETE SET NULL would erase which rung a historical session actually got, which is the entire reason the column exists), so one historical session refuses the delete at the database. A client must treat a non-zero session_count exactly like a non-empty used_by when deciding whether to offer Delete.
Everything referencing this launch profile. Non-empty in any dimension means DELETE is 409.
object
object
True when stream_profile_policy.global_default_profile_id points here.
Count of user_profile_preferences rows pointing here.
UI-P4: a non-fatal advisory returned on an admin profile write. A warning NEVER fails the request - it reports a configuration that is legal but almost certainly not what the operator meant.
object
H264_floor_not_last - every rung after the H.264 rung is unreachable, because H.264 passes every clamp. floor_not_least_demanding - the H.264 rung has a higher min_offer_bandwidth_kbps or min_decode_height than a rung above it, or requires a hardware encoder while a rung above it does not; a floor harder to satisfy than the rung above it is a misconfiguration.
Example
{ "visibility": "user", "rungs": [ { "stream_profile": { "codec": "h264", "high_refresh_display": "none", "browser_client": "recommended", "warnings": [ { "code": "h264_floor_not_last" } ], "visibility": "user" } } ], "warnings": [ { "code": "h264_floor_not_last" } ]}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" } ] }}Authenticated but insufficient role / not the owner (precedes resource lookup).
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" } ] }}No such resource.
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" } ] }}