Edit an encode rung.
const url = 'http://localhost:8080/v1/admin/stream-profiles/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0';const options = { method: 'PATCH', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"id":"example","display_name":"example","codec":"h264","width":1,"height":1,"fps":1,"h264_profile":"example","nominal_bitrate_kbps":1,"min_offer_bandwidth_kbps":1,"recommended_offer_bandwidth_kbps":1,"headroom_factor":1,"abr_floor_kbps":1,"max_startup_rtt_ms":1,"min_decode_height":1,"high_refresh_display":"none","hardware_encoder_required":true,"browser_client":"recommended","playout0_ms":1,"visibility":"user"}'};
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/stream-profiles/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0";
let payload = json!({ "id": "example", "display_name": "example", "codec": "h264", "width": 1, "height": 1, "fps": 1, "h264_profile": "example", "nominal_bitrate_kbps": 1, "min_offer_bandwidth_kbps": 1, "recommended_offer_bandwidth_kbps": 1, "headroom_factor": 1, "abr_floor_kbps": 1, "max_startup_rtt_ms": 1, "min_decode_height": 1, "high_refresh_display": "none", "hardware_encoder_required": true, "browser_client": "recommended", "playout0_ms": 1, "visibility": "user" });
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/stream-profiles/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0 \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "id": "example", "display_name": "example", "codec": "h264", "width": 1, "height": 1, "fps": 1, "h264_profile": "example", "nominal_bitrate_kbps": 1, "min_offer_bandwidth_kbps": 1, "recommended_offer_bandwidth_kbps": 1, "headroom_factor": 1, "abr_floor_kbps": 1, "max_startup_rtt_ms": 1, "min_decode_height": 1, "high_refresh_display": "none", "hardware_encoder_required": true, "browser_client": "recommended", "playout0_ms": 1, "visibility": "user" }'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 for one encode rung.
object
Required on POST (rung ids are operator-chosen text, not uuids); ignored on PATCH.
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.
Responses
Section titled “Responses”OK.
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.
Example
{ "codec": "h264", "high_refresh_display": "none", "browser_client": "recommended", "warnings": [ { "code": "h264_floor_not_last" } ], "visibility": "user"}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" } ] }}