Update instance settings.
const url = 'http://localhost:8080/v1/admin/settings';const options = { method: 'PATCH', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"steam_preparation_enabled":true,"registration_mode":"closed","storage_provider":"auto","library_discovery_enabled":true,"library_discovery_interval_minutes":1,"library_discovery_appdetails_enabled":true,"mic_capture_enabled":true,"image_update_policy":"manual","allowed_origins":["example"],"release_channel":"stable","release_edge_branch":"example","release_check":"daily","release_webhook_enabled":true,"release_webhook_url":"example","platform_auto_apply":true,"steam_warm_enabled":true,"steam_warm_rewarm":"library_open","steam_warm_idle_minutes":1,"default_timezone":"example","default_language":"example","default_region_format":"example","default_keyboard":{"layout":"example","variant":"example"},"default_match_cursor":"auto","default_mouse_sensitivity":1,"default_raw_mouse":true,"default_flip_scroll":true,"default_nintendo_layout":true}'};
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/settings";
let payload = json!({ "steam_preparation_enabled": true, "registration_mode": "closed", "storage_provider": "auto", "library_discovery_enabled": true, "library_discovery_interval_minutes": 1, "library_discovery_appdetails_enabled": true, "mic_capture_enabled": true, "image_update_policy": "manual", "allowed_origins": ("example"), "release_channel": "stable", "release_edge_branch": "example", "release_check": "daily", "release_webhook_enabled": true, "release_webhook_url": "example", "platform_auto_apply": true, "steam_warm_enabled": true, "steam_warm_rewarm": "library_open", "steam_warm_idle_minutes": 1, "default_timezone": "example", "default_language": "example", "default_region_format": "example", "default_keyboard": json!({ "layout": "example", "variant": "example" }), "default_match_cursor": "auto", "default_mouse_sensitivity": 1, "default_raw_mouse": true, "default_flip_scroll": true, "default_nintendo_layout": true });
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/settings \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "steam_preparation_enabled": true, "registration_mode": "closed", "storage_provider": "auto", "library_discovery_enabled": true, "library_discovery_interval_minutes": 1, "library_discovery_appdetails_enabled": true, "mic_capture_enabled": true, "image_update_policy": "manual", "allowed_origins": [ "example" ], "release_channel": "stable", "release_edge_branch": "example", "release_check": "daily", "release_webhook_enabled": true, "release_webhook_url": "example", "platform_auto_apply": true, "steam_warm_enabled": true, "steam_warm_rewarm": "library_open", "steam_warm_idle_minutes": 1, "default_timezone": "example", "default_language": "example", "default_region_format": "example", "default_keyboard": { "layout": "example", "variant": "example" }, "default_match_cursor": "auto", "default_mouse_sensitivity": 1, "default_raw_mouse": true, "default_flip_scroll": true, "default_nintendo_layout": true }'Authorizations
Section titled “Authorizations”Request Bodyrequired
Section titled “Request Bodyrequired”object
Controls automatic preparation and template consumption for eligible adopted Steam images. Absent unchanged; null invalid. Revision is read-only.
Managed-home backing store: auto = local when the session host has an effective home root, volume otherwise. Affects new homes only.
Steam library discovery Phase 4. EVERY FIELD ON THIS BODY IS OPTIONAL AND ABSENCE MEANS UNCHANGED - a plain (non-pointer) decode of an absent library_discovery_enabled would read false and silently switch discovery off every time an admin changed the registration mode. A PATCH naming no known field is a no-op that returns current state.
Admin-libraries amendment (2026-08-01). Absent = unchanged. Out of bounds (below 15 or above 10080 minutes) is 400 validation_failed. Setting it does not lift an environment override - LibraryStatus reports the resolved value.
Admin-libraries amendment (2026-08-01). Absent = unchanged (pointer decode, same rule as library_discovery_enabled above).
Microphone capture amendment (2026-08-02). Absent = unchanged (pointer decode). Default false. Gates the POST /v1/sessions mic request; affects subsequent launches only.
P3 (image management). Absent = unchanged (pointer decode). Applied by POST /v1/admin/images/sync — see the App-image management P3 section.
First-run wizard v2 §S6e (migration 0064). The signaling origin allow-list. ABSENT = UNCHANGED; an explicitly-sent [] CLEARS the list. Those are different requests and the server distinguishes them (pointer decode), so a PATCH that only changes the registration mode can never wipe the allow-list. Each entry must be scheme + host only (http/https, no path, query, credentials or trailing slash); the server stores the NORMALIZED form, so what is saved is exactly what /v1/signal compares against. “*” IS REJECTED OUTRIGHT with 400 validation_failed - a wildcard would discard the layer entirely. A bad entry is 400 naming its position, and nothing is written. Setting this does NOT lift an environment override: when QUASAR_ALLOWED_ORIGINS is SET it wins, and GET /v1/admin/access-check reports which source is in force.
Platform-release amendment 1 (#104/#106, migration 0074); beta added by amendment 3 (#121, migration 0079); preview added by amendment 48 (#840, migration 0107). Which platform releases the admin console is shown. Absent = unchanged (pointer decode, the same rule every field on this body follows - a plain decode would read “” and silently reset the channel whenever an admin changed the registration mode). Any value outside the enum is 400 validation_failed. Takes effect immediately: the next GET /v1/admin/platform/releases reads the other channel’s rows. IT DOES NOT TRIGGER DETECTION - that is a jobs-framework job, and “check now” is POST /v1/admin/jobs/{job_id}/run.
Platform-release amendment 1 (#104/#106, migration 0074). The branch the EDGE channel follows; default develop. Absent = unchanged. Validated as a git ref name component - non-empty, at most 255 characters, no whitespace, no “..”, no leading “-”, no control characters - 400 validation_failed otherwise. Validated and stored whatever the channel is, and NEVER CLEARED BY A CHANNEL SWITCH, so an operator who visits stable and comes back keeps their branch.
Amendment 48 (#840, migration 0107). How often platform.release_detect runs. Absent = unchanged; explicit null = follow the channel again (daily on preview, weekly on every other channel); any other value is 400 validation_failed. Never changed by a channel switch. The job takes its period from it (locked_by instance_settings.release_check); its window stays the Jobs tab’s, and on daily the window opens every day.
Release notifications, ADDITIVE (#123, migration 0080). Whether a newly detected platform release is POSTed to release_webhook_url. Absent = unchanged. Setting it TRUE is 400 validation_failed whenever the URL this request LEAVES BEHIND is empty - nothing stored and none supplied, or an explicit “” in the same body - because a switch that silently does nothing is worse than a refusal. Default false, so an instance that has never been configured announces nothing.
Release notifications, ADDITIVE (#123, migration 0080). Where one release notification is POSTed. Absent = unchanged; an explicitly-sent “” CLEARS it AND SETS release_webhook_enabled false in the same write. Any other value must be an absolute https URL with no userinfo, at most 2048 characters, or 400 validation_failed - http, a credential in the URL and a relative reference are all refused. THE SERVER STILL CONTAINS THE REQUEST AT SEND TIME: delivery refuses any host that resolves to a loopback, private, link-local or multicast address, follows no redirect, and bounds the response body. THE URL IS TREATED AS A CREDENTIAL (a Slack or Discord webhook URL authenticates by being known), so it never appears in a log line, an audit record or a delivery error.
Unattended automatic apply, ADDITIVE (amendment 8, #122, migration 0081). Whether a detected platform release is applied WITHOUT a click. Absent = unchanged. Default false, so an instance that never opts in behaves exactly as before. It depends on nothing else being configured first, so unlike release_webhook_enabled there is no companion 400: with nothing to apply it applies nothing and records why. NO WINDOW FIELD ACCOMPANIES IT, deliberately - an unattended pass runs on a successful platform.release_detect job, so that job’s own schedule IS the window, and it is already editable in the Jobs tab.
Warm Steam master switch, ADDITIVE (amendment 39, #759, migration 0105). Absent = unchanged. Off stops creating parked sessions, and the session.parked_expire job stops the existing ones at its next run.
Amendment 39. What a non-Steam launch that stopped a parked session leaves behind (control-api.md §Warm Steam, “Non-Steam apps”). Absent = unchanged; any other value is 400 validation_failed.
Amendment 39. Each heartbeat, and a park on exit, sets a parked session’s deadline to now plus this many minutes. Absent = unchanged; outside 1-60 is 400 validation_failed.
Amendment 49 (#845). As RegionSettings.timezone. Default UTC. Absent = unchanged.
Amendment 49. Default en-US. Absent = unchanged.
Amendment 49. null = the same as the language (the default). Absent = unchanged.
object
One xkeyboard-config layout name, e.g. gb.
One variant of that layout, e.g. extd; empty for the basic variant.
Amendment 49. Default auto.
Amendment 49. Default 1.
Amendment 49. Default false.
Amendment 49. Default false.
Amendment 49. Default false.
Responses
Section titled “Responses”OK.
object
object
Managed-home backing store: auto = local when the session host has an effective home root, volume otherwise. Affects new homes only.
P3 (image management). Instance-wide update policy applied by POST /v1/admin/images/sync. Column exists since migration 0054 (DDL default notify) but only reaches the wire from P3. Optional in the envelope so pre-P3 servers stay conformant.
Microphone capture amendment (2026-08-02). Instance-wide gate for the POST /v1/sessions mic request. Default false (ship-dark). Server-enforced at launch; flipping it affects subsequent launches only.
Admin-libraries amendment (2026-08-01). The operator-set scan interval, minutes, default 360. THE DATABASE COLUMN IS THE OPERATOR PATH AND THE ENV VAR IS AN OVERRIDE, NOT A DEFAULT: when QUASAR_LIBRARY_SCAN_INTERVAL is set it wins - and its documented 0 = hard-kill-regardless-of-the-database-flag semantics are unchanged. LibraryStatus.scan_interval_secs remains the RESOLVED value (database-then-env-override), and LibraryStatus.interval_overridden_by_env says which source won so a UI can grey a control the environment has pinned.
Admin-libraries amendment (2026-08-01). The operator-set third-party appdetails lookup switch, default false - the same privacy decision QUASAR_STEAM_APPDETAILS_LOOKUP carried, now settable from the admin UI. Same override rule as the interval: a SET env var wins (a privacy-hardened deployment can pin it off in the environment), and LibraryStatus.appdetails_overridden_by_env reports when that happened.
Steam library discovery Phase 4 (migration 0045). THE MASTER SWITCH, AND THE ONLY SWITCH. Default false - ship-dark, the posture artwork already holds - and with it false the scheduler returns before its first query and the agent pull answers an empty list: no scan rows, no agent work, no third-party calls. Under operator decision 1 AUTO-PUBLISH IS THE BEHAVIOUR, NOT A MODE (there is no review queue), so there is deliberately no second “publish” toggle: its false branch would select a path that was never built. Read PER PASS and per request rather than cached at boot, so flipping it in the admin UI takes effect without a restart.
First-run wizard v2 §S6e (migration 0064). The admin-editable signaling origin allow-list, normalized (scheme + lowercased host). THIS IS THE DATABASE COLUMN, NOT NECESSARILY WHAT /v1/signal ENFORCES: QUASAR_ALLOWED_ORIGINS, when SET, overrides it outright - including when set to the empty string, which is how a hardened deployment pins the list off. That override rule is what makes the migration a behavioural no-op on upgrade for every existing deployment. GET /v1/admin/access-check reports the RESOLVED list plus which source won, so a UI can grey out a control the environment has pinned - the same shape library_discovery_interval_minutes uses. AN EMPTY LIST IS NOT “DENY ALL”: /v1/signal still admits a same-origin request and a request with no Origin header at all, so a fresh instance with nothing configured works. Optional in the envelope so pre-amendment servers stay conformant.
Platform-release amendment 1 (#104/#106, migration 0074); beta added by amendment 3 (#121, migration 0079); preview added by amendment 48 (#840, migration 0107). The instance’s platform- release channel: stable = tagged, noted releases; beta = those AND the prereleases among them; edge = whatever was last published from release_edge_branch, with no notes; preview = every build of develop, with its commit list. DEFAULT stable - an instance that has never been configured is not shown prereleases or branch builds. Read per request rather than at boot, so a switch needs no restart. Optional in the envelope so pre-amendment servers stay conformant.
Platform-release amendment 1 (#104/#106, migration 0074). The branch the edge channel follows; default develop. Reported whatever the channel is (it selects nothing while the channel is stable) so a UI can render the control without a second read. Optional in the envelope so pre-amendment servers stay conformant.
Amendment 48 (#840, migration 0107). How often platform.release_detect runs. NULL, THE DEFAULT, FOLLOWS THE CHANNEL: daily on preview, weekly on every other channel. Optional in the envelope so pre-amendment servers stay conformant; a client reads absent as null.
Release notifications, ADDITIVE (#123, migration 0080). Whether a detected platform release is announced to release_webhook_url. Default false. Optional in the envelope so a pre-#123 server stays conformant; a client reads absent as false.
Release notifications, ADDITIVE (#123, migration 0080). The configured webhook URL, or “” when none is set. It is admin-only, as this whole envelope is. THE SIGNING SECRET IS NOT HERE and never will be: it is an instance_secrets row read through GET /v1/admin/secrets, which reports configured/readable and a masked hint, never a value.
Unattended automatic apply, ADDITIVE (amendment 8, #122, migration 0081). Whether a detected platform release is applied without a click. Default false. Optional in the envelope so a pre-#122 server stays conformant; a client reads absent as false.
Warm Steam, ADDITIVE (amendment 39, #759, migration 0105). Default false. Optional in the envelope so a pre-amendment server stays conformant; absent reads as false.
Amendment 39. Default library_open.
Amendment 39. Default 10.
Amendment 49 (#845). Default UTC. Optional in the envelope so a pre-amendment server stays conformant.
Amendment 49. Default en-US.
Amendment 49. null = the same as the language.
object
One xkeyboard-config layout name, e.g. gb.
One variant of that layout, e.g. extd; empty for the basic variant.
Example
{ "settings": { "steam_preparation_enabled": true, "registration_mode": "closed", "storage_provider": "auto", "image_update_policy": "manual", "release_channel": "stable", "release_check": "daily", "steam_warm_rewarm": "library_open", "default_match_cursor": "auto" }}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, 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.
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. 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.
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
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.
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.
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, 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.
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. 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.
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
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.
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).
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, 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.
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. 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.
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
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.
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" }}