Skip to content

Developer apply - an arbitrary digest set to one owned target (admin).

POST
/v1/admin/platform/developer-apply
curl --request POST \
--url http://localhost:8080/v1/admin/platform/developer-apply \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "target": "control_plane", "host_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "components": [ { "name": "example", "image": "example", "digest": "example" } ], "force": true, "external_backup_confirmed": true }'

AMENDMENT 14, OWNER ADDITION on #353 (2026-09-25), beyond #352 decision 24; served since RH06-08 (#360). The product lane (#352 decisions 15 and 22): a standalone attempt that applies the requested digests to ONE owned target - the control plane or one host - ordered recovery-actor first. Every image digest-only and under the namespace allowlist (ADR 0001; the recovery actor’s allowlist is the enforcement, the control plane checks up front). ADR 0002 holds on the images’ build-identity labels: a control-plane digest below the installed schema is refused, one above it MIGRATES and follows #352 decision 14 (drain, then the pre-update dump or external_backup_confirmed); a request that does not name control-plane, whatever its target, must carry the installed control plane’s commit or a known release’s commit at or below it; a control_plane target names recovery-actor only together with control-plane, and a request for the agent on the control plane’s own machine names only node-agent. Never offered as a release, never unattended. Carries no release version, so under signature mode require it fails signature_missing; under verify it applies unsigned with the WARN. Recorded as a kind developer_apply attempt; audited as platform.apply.developer.

Media typeapplication/json

Amendment 14, owner addition on #353. The body of POST /v1/admin/platform/developer-apply.

object
target
required
string
Allowed values: control_plane host
host_id

Required when target is host; absent otherwise.

string format: uuid
components
required

Each name at most once. A control_plane target may name control-plane and recovery-actor, but names recovery-actor ONLY TOGETHER WITH control-plane (A1); a host target may name node-agent and recovery-actor, except that a request for the agent on the control plane’s own machine names ONLY node-agent (that machine’s actor moves in the control-plane step). Either violation is 400 validation_failed. A request that does not name control-plane, whatever its target, is bound by the host commit rule: the installed control plane’s own commit, or a known release’s commit at or below it. ORDER IS NOT SIGNIFICANT: the control plane orders recovery-actor first, as for every apply.

Array<object>
>= 1 items <= 2 items

One component of a platform release, pinned. Same shape as ReleaseManifestComponent and as agent-api.md release_apply.components.

object
name
required

The component. Only “node-agent” is ever sent to a registry host; an owned host (amendment 14, #353) may also be sent “recovery-actor”, and the ORDER of the list is the order of replacement. “control-plane” is applied by the updater or recovery actor beside the control plane and never over an agent connection.

string
image
required

Registry repository reference with NO TAG AND NO DIGEST. Consumers compose image@digest (ADR 0001).

string
digest
required

Sha256: followed by 64 lowercase hex.

string
force

Optional; absent means false. As on the per-host and fleet applies.

boolean
external_backup_confirmed

Optional; absent means false. As on the fleet apply: read only for a migrating control-plane digest on an external database.

boolean

Accepted - the attempt was created.

Media typeapplication/json
object
attempt
required

One attempt to move ONE target - the control plane, or one host - to one digest set (schema.md platform_apply_attempts). Every apply produces one: fleet or standalone, apply or revert, success or failure.

object
id
required
string format: uuid
run_id
required

The fleet run this belongs to. NULL for a standalone per-host apply or revert - those are attempts with no run, because a run carries fleet ordering and a single-host action has none.

string | null format: uuid
kind
required

A REVERT IS AN APPLY WITH AN OLDER DIGEST SET - same wire message, same states, same reasons. This field exists so history can say which button was pressed, and for nothing else. AMENDMENT 9 (#185) APPENDS auto_revert: no button was pressed - the host’s UPDATER put the previous digests back itself after the new agent container failed its health wait (agent-api.md release_state restored), and the control plane wrote this row beside the failed apply so the history shows both steps. It is recorded succeeded on insert (the updater reports restored only for a restore that came up; a restore that itself failed leaves no row and both failures in the failed apply’s output), was never driven over the wire, and its requested_digests are the failed apply’s previous_digests. Amendment 14 (#353): on an owned host the recovery actor did the restore, and the row names ONLY the restored component (the one whose replacement failed), which may be recovery-actor. Amendment 14 (owner addition on #353) APPENDS developer_apply: an admin applied an arbitrary digest set to an owned target (POST /v1/admin/platform/developer-apply); release_id is null, and it is otherwise an ordinary apply and revert source. A client meeting an unrecognized kind renders it verbatim.

string
Allowed values: apply revert auto_revert developer_apply
target
required
string
Allowed values: control_plane host
host_id
required

Null exactly when target is control_plane.

string | null format: uuid
node_name
required
string | null
release_id
required

The release the digests came from, when one is still known. NULL is legitimate on a revert, whose digest set may correspond to no release row this instance still has - requested_digests, not this field, is the authority for what an attempt did.

string | null format: uuid
requested_digests
required

What was asked for, in manifest order - the exact components array sent on agent-api.md release_apply. THE AUTHORITY for what this attempt did.

Array<object>

One component of a platform release, pinned. Same shape as ReleaseManifestComponent and as agent-api.md release_apply.components.

object
name
required

The component. Only “node-agent” is ever sent to a registry host; an owned host (amendment 14, #353) may also be sent “recovery-actor”, and the ORDER of the list is the order of replacement. “control-plane” is applied by the updater or recovery actor beside the control plane and never over an agent connection.

string
image
required

Registry repository reference with NO TAG AND NO DIGEST. Consumers compose image@digest (ADR 0001).

string
digest
required

Sha256: followed by 64 lowercase hex.

string
previous_digests
required

What the target was on before. Empty until first reported. What a revert reads back, and what the manual restore recipe in a failure is copied from.

Array<object>

What a component was on BEFORE an attempt. Recorded from every release_state, in every state and not only failing ones, because the previous digests are what restores a half-failed stack and something has to have written them down before the restore is needed (#113 prototype finding 5). This is what a revert reads.

object
name
required
string
digest
required

Null when the updater could not determine it. NEVER OMITTED - a client must be able to tell “nothing was there” from “nobody looked”.

string | null
state
required

One target’s attempt state. The six middle values are EXACTLY agent-api.md release_state.state, relayed unchanged. queued and waiting_sessions are control-plane-only and precede the wire (the command has not been sent); cancelled applies ONLY to an attempt a cancel caught in one of those two states - CANCEL NEVER INTERRUPTS AN ATTEMPT THAT HAS BEEN SENT. Amendment 14 (#353) rewords, without changing any value: recreating means the target’s actor is replacing a component’s container (on an owned machine the old one is stopped and kept until the new one is verified), not a Compose command.

string
Allowed values: queued waiting_sessions pending pulling recreating verifying succeeded failed cancelled
reason
required
One of:

Why an attempt failed. A CLOSED vocabulary of STABLE IDENTIFIERS the UI maps to text, shared verbatim with agent-api.md release_state.reason and with the release_apply ack’s error, so ONE client-side mapping serves the wire, this API and the history. Non-null exactly when the state is failed. “unsupported” is written by the control plane and never sent on the wire: no ack arrived within the 10s ack timeout, so the agent build predates the amendment. A client meeting an unrecognized value renders it verbatim. “signature_missing” and “signature_invalid” are amendment 5’s two APPENDED values (#120): a host that requires a signed release met an unsigned one, and a manifest signature that did not verify (bad signature, untrusted key, digests not named by the signed manifest, a signature that could not be fetched, or verification on with no trusted keys - all fail closed). Signature verification is OFF BY DEFAULT (ADR 0003), so neither occurs unless an operator turns it on. AMENDMENT 14 (#353) APPENDS five values, produced only by an owned machine and each meaning “nothing changed”: recipe_unsupported (the recovery actor lacks the recipe revision an image declares, ADR 0008), owner_conflict (a Quasar-looking container without this installation’s labels is in the way), backup_failed (control plane, migrating release, Quasar-owned database: the pre-update dump could not be taken), backup_unconfirmed (control plane, migrating release, external database, no external_backup_confirmed) and interrupted (the actor, engine or machine restarted before the old container was taken out of service; settled, never retried). Amendment 14 also confirms signature_missing and signature_invalid as members. Full per-value semantics: agent-api.md §release_state.

string
Allowed values: updater_absent busy invalid namespace_rejected digest_malformed pull_failed recreate_failed never_started unhealthy updater_unreachable timeout unsupported signature_missing signature_invalid recipe_unsupported owner_conflict backup_failed backup_unconfirmed interrupted
sessions_remaining
required

The N in “waiting on N sessions”: the last observed non-terminal session count while state is waiting_sessions - ON THIS HOST for a host target, and FLEET-WIDE for the control-plane target, whose recreate is instance-wide. On a control-plane target it is set ONLY when the release’s migrates is true (amendment 6, #153): a recreate alone no longer ends a running session, so a non-migrating control-plane attempt never enters waiting_sessions and leaves this null throughout. A CLIENT MUST SHOW IT IN A FORCE CONFIRMATION - force is the operator agreeing to end N live sessions, and a confirmation that does not name N is not informed consent. Advisory and last-observed, never a gate. Null once the attempt has been sent.

integer | null
force
required

Skip the zero-sessions wait and stop what is running. The agent does no session logic on it: it records which decision the control plane made.

boolean
output
required

The bounded tail (last 8192 bytes, truncated from the front at a line boundary) of the failing step’s output. “” when there is nothing to report. NEVER a credential or an environment value. Amendment 14 (#353): a failed MIGRATING control-plane attempt on an owned machine is never restored automatically, and its output ends with the one-line restore command naming pre_update_dump and the version it returns to.

string
pre_update_dump

ADDITIVE (amendment 14, #353). The DUMP REFERENCE: the name of the pre-update dump the recovery actor took before replacing the control plane with a migrating release, exactly as the operator passes it to the restore command. Opaque - display it, never parse it. Null on every host attempt, on a non-migrating or external-database control-plane attempt, and on a registry control plane. A server implementing the amendment always serializes it; optional here so a client still accepts an older server (absent reads as null).

string | null
<= 255 characters
requested_by
required
string | null format: uuid
created_at
required
string format: date-time
started_at
required
string | null format: date-time
finished_at
required
string | null format: date-time
Example
{
"attempt": {
"kind": "apply",
"target": "control_plane",
"state": "queued",
"reason": "updater_absent"
}
}

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, host_not_ready, restart_required, stale_revision, unsupported_source, approval_superseded, approval_expired, attempt_conflict, cancel_too_late, retry_exhausted, home_conflict, placement_ineligible, inherited_placement, recovery_uncertain, 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. Also omitted when the holder is the caller’s parked session or released runtime (amendment 39). 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
reason

Amendment 40 (#602), ADDITIVE: a machine-readable cause under code. On 503 no_host_available, home_host_unavailable: the launch is pinned to the host that holds the caller’s home for this app (Amendment 53: or it is pinned to the host of the caller’s Steam home for a sharing launch) and that host is offline or no longer enrolled. Only an admin clears it, so a client does not spend its automatic retry on it. Names no host. Amendment 53 (#1001), on 409 home_in_use: steam_library_open_in_desktop (the holder mounts the Steam library as a shared home) or steam_game_running (the refused launch shares the library and a Steam session holds it); absent when the holder is already stopping. Open string: a client treats an unknown value as absent.

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"
}
],
"reason": "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, host_not_ready, restart_required, stale_revision, unsupported_source, approval_superseded, approval_expired, attempt_conflict, cancel_too_late, retry_exhausted, home_conflict, placement_ineligible, inherited_placement, recovery_uncertain, 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. Also omitted when the holder is the caller’s parked session or released runtime (amendment 39). 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
reason

Amendment 40 (#602), ADDITIVE: a machine-readable cause under code. On 503 no_host_available, home_host_unavailable: the launch is pinned to the host that holds the caller’s home for this app (Amendment 53: or it is pinned to the host of the caller’s Steam home for a sharing launch) and that host is offline or no longer enrolled. Only an admin clears it, so a client does not spend its automatic retry on it. Names no host. Amendment 53 (#1001), on 409 home_in_use: steam_library_open_in_desktop (the holder mounts the Steam library as a shared home) or steam_game_running (the refused launch shares the library and a Steam session holds it); absent when the holder is already stopping. Open string: a client treats an unknown value as absent.

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"
}
],
"reason": "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, host_not_ready, restart_required, stale_revision, unsupported_source, approval_superseded, approval_expired, attempt_conflict, cancel_too_late, retry_exhausted, home_conflict, placement_ineligible, inherited_placement, recovery_uncertain, 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. Also omitted when the holder is the caller’s parked session or released runtime (amendment 39). 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
reason

Amendment 40 (#602), ADDITIVE: a machine-readable cause under code. On 503 no_host_available, home_host_unavailable: the launch is pinned to the host that holds the caller’s home for this app (Amendment 53: or it is pinned to the host of the caller’s Steam home for a sharing launch) and that host is offline or no longer enrolled. Only an admin clears it, so a client does not spend its automatic retry on it. Names no host. Amendment 53 (#1001), on 409 home_in_use: steam_library_open_in_desktop (the holder mounts the Steam library as a shared home) or steam_game_running (the refused launch shares the library and a Steam session holds it); absent when the holder is already stopping. Open string: a client treats an unknown value as absent.

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"
}
],
"reason": "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, host_not_ready, restart_required, stale_revision, unsupported_source, approval_superseded, approval_expired, attempt_conflict, cancel_too_late, retry_exhausted, home_conflict, placement_ineligible, inherited_placement, recovery_uncertain, 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. Also omitted when the holder is the caller’s parked session or released runtime (amendment 39). 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
reason

Amendment 40 (#602), ADDITIVE: a machine-readable cause under code. On 503 no_host_available, home_host_unavailable: the launch is pinned to the host that holds the caller’s home for this app (Amendment 53: or it is pinned to the host of the caller’s Steam home for a sharing launch) and that host is offline or no longer enrolled. Only an admin clears it, so a client does not spend its automatic retry on it. Names no host. Amendment 53 (#1001), on 409 home_in_use: steam_library_open_in_desktop (the holder mounts the Steam library as a shared home) or steam_game_running (the refused launch shares the library and a Steam session holds it); absent when the holder is already stopping. Open string: a client treats an unknown value as absent.

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"
}
],
"reason": "example"
}
}

Run_active; attempt_in_flight; host_not_eligible (with the host’s EligibilityReason, including release_above_control_plane for a commit that cannot be shown not to be ahead and below_floor); preflight_blocked (the control-plane target); target_not_owned (the target is not an owned install); image_unresolvable (a digest does not resolve as seen from the control plane, carries no readable build identity, or the images disagree on their commit); namespace_rejected (an image outside the allowlist).

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, host_not_ready, restart_required, stale_revision, unsupported_source, approval_superseded, approval_expired, attempt_conflict, cancel_too_late, retry_exhausted, home_conflict, placement_ineligible, inherited_placement, recovery_uncertain, 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. Also omitted when the holder is the caller’s parked session or released runtime (amendment 39). 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
reason

Amendment 40 (#602), ADDITIVE: a machine-readable cause under code. On 503 no_host_available, home_host_unavailable: the launch is pinned to the host that holds the caller’s home for this app (Amendment 53: or it is pinned to the host of the caller’s Steam home for a sharing launch) and that host is offline or no longer enrolled. Only an admin clears it, so a client does not spend its automatic retry on it. Names no host. Amendment 53 (#1001), on 409 home_in_use: steam_library_open_in_desktop (the holder mounts the Steam library as a shared home) or steam_game_running (the refused launch shares the library and a Steam session holds it); absent when the holder is already stopping. Open string: a client treats an unknown value as absent.

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"
}
],
"reason": "example"
}
}

Release_below_schema_version - a control-plane digest below the installed schema (ADR 0002).

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, host_not_ready, restart_required, stale_revision, unsupported_source, approval_superseded, approval_expired, attempt_conflict, cancel_too_late, retry_exhausted, home_conflict, placement_ineligible, inherited_placement, recovery_uncertain, 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. Also omitted when the holder is the caller’s parked session or released runtime (amendment 39). 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
reason

Amendment 40 (#602), ADDITIVE: a machine-readable cause under code. On 503 no_host_available, home_host_unavailable: the launch is pinned to the host that holds the caller’s home for this app (Amendment 53: or it is pinned to the host of the caller’s Steam home for a sharing launch) and that host is offline or no longer enrolled. Only an admin clears it, so a client does not spend its automatic retry on it. Names no host. Amendment 53 (#1001), on 409 home_in_use: steam_library_open_in_desktop (the holder mounts the Steam library as a shared home) or steam_game_running (the refused launch shares the library and a Steam session holds it); absent when the holder is already stopping. Open string: a client treats an unknown value as absent.

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"
}
],
"reason": "example"
}
}

Apply_unsupported - as for the per-host apply.

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, host_not_ready, restart_required, stale_revision, unsupported_source, approval_superseded, approval_expired, attempt_conflict, cancel_too_late, retry_exhausted, home_conflict, placement_ineligible, inherited_placement, recovery_uncertain, 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. Also omitted when the holder is the caller’s parked session or released runtime (amendment 39). 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
reason

Amendment 40 (#602), ADDITIVE: a machine-readable cause under code. On 503 no_host_available, home_host_unavailable: the launch is pinned to the host that holds the caller’s home for this app (Amendment 53: or it is pinned to the host of the caller’s Steam home for a sharing launch) and that host is offline or no longer enrolled. Only an admin clears it, so a client does not spend its automatic retry on it. Names no host. Amendment 53 (#1001), on 409 home_in_use: steam_library_open_in_desktop (the holder mounts the Steam library as a shared home) or steam_game_running (the refused launch shares the library and a Steam session holds it); absent when the holder is already stopping. Open string: a client treats an unknown value as absent.

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"
}
],
"reason": "example"
}
}