Skip to content

UI-P4: edit a launch profile, including reordering its rungs (order IS preference).

PATCH
/v1/admin/launch-profiles/{id}
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" ] }'
id
required
string format: uuid
Media typeapplication/json

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
id

Required on POST; ignored on PATCH.

string
display_name
required
string
description
string
visibility
string
Allowed values: user debug internal
sort_order
integer
rungs
required

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.

Array<string>
>= 1 items

OK.

Media typeapplication/json
object
id
required
string
display_name
required
string
description
required
string
visibility
required

Debug/internal launch profiles are never returned by GET /v1/me/profiles.

string
Allowed values: user debug internal
sort_order
required
integer
rungs
required
Array<object>
>= 1 items
object
position
required

Preference order, 1 = tried first. Server-assigned from the ordered id array on write; a client never sends positions.

integer
>= 1
stream_profile
required

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
id
required

Rungs created by migration 0036’s fan-out use ‘-’, e.g. ‘1080p60-h264’.

string
display_name
required
string
codec
required

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.

string
Allowed values: h264 hevc av1
width
required
integer
height
required
integer
fps
required
integer
h264_profile
required

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.

string
nominal_bitrate_kbps
required
integer
min_offer_bandwidth_kbps
required
integer
recommended_offer_bandwidth_kbps
required
integer
headroom_factor
required
number
abr_floor_kbps
required
integer
max_startup_rtt_ms
required
integer
min_decode_height
required
integer
high_refresh_display
required
string
Allowed values: none recommended required
hardware_encoder_required
required
boolean
browser_client
required
string
Allowed values: recommended supported risky
playout0_ms
required
integer
warnings

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.

Array<object>

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
code
required

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.

string
Allowed values: h264_floor_not_last floor_not_least_demanding
message
required
string
visibility
required

Rungs created by the fan-out are ‘internal’: a rung is never offered standalone, only via the launch profile that lists it.

string
Allowed values: user debug internal
used_by

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.

Array<object>

A minimal reference to a profile object, used in ‘used by’ lists.

object
id
required
string
display_name
required
string
session_count

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.

integer
used_by

Everything referencing this launch profile. Non-empty in any dimension means DELETE is 409.

object
apps
required
Array<object>
object
id
required
string format: uuid
name
required
string
global_default
required

True when stream_profile_policy.global_default_profile_id points here.

boolean
user_preferences
required

Count of user_profile_preferences rows pointing here.

integer
warnings
Array<object>

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
code
required

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.

string
Allowed values: h264_floor_not_last floor_not_least_demanding
message
required
string
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.

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

Authenticated but insufficient role / not the owner (precedes resource lookup).

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