Skip to content

Override an app's artwork (fuzzy matching WILL be wrong sometimes).

PUT
/v1/admin/apps/{id}/artwork
curl --request PUT \
--url http://localhost:8080/v1/admin/apps/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/artwork \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "provider_ref": "example", "tile_url": "example", "hero_url": "example", "rematch": true, "force": true }'

UI-P7. Exactly one intent per request. provider_ref accepts a candidate from POST …/artwork/search. tile_url/hero_url fetch operator-supplied art - the path for anything a games database does not have, including every desktop app. rematch re-runs automatic matching now instead of waiting for the background sweep; it REFUSES a locked record with 409 unless force is also sent (#385 - it previously cleared unconditionally, silently discarding the correction the flag exists to protect). An operator-supplied URL is ATTACKER-ADJACENT input (an operator pastes what they were sent) and goes through the same guards as a provider URL with no exception for admin privilege: http(s) only, resolved address must be publicly routable (loopback, RFC1918, link-local, CGNAT and cloud-metadata addresses are refused, including across redirects), at most 3 redirects, a byte cap, and the stored bytes must SNIFF as JPEG/PNG/WebP regardless of the Content-Type claimed. Any override sets locked=true, so the background sweep can never silently re-break the correction.

id
required
string format: uuid
Media typeapplication/json

Exactly one intent per request; an empty body is 400 validation_failed.

object
provider_ref

Accept this candidate from a prior …/artwork/search.

string
tile_url

Fetch operator-supplied art for the tile crop. Absent leaves the existing tile untouched.

string
hero_url

Fetch operator-supplied art for the hero crop. Absent leaves the existing hero untouched.

string
rematch

Re-run automatic matching now instead of waiting for the background sweep. 409 conflict when the record is locked, unless force is also sent.

boolean
force

Qualifies rematch ONLY: replace a locked record. The other intents set locked themselves and were never blocked by it.

boolean
Examplegenerated
{
"provider_ref": "example",
"tile_url": "example",
"hero_url": "example",
"rematch": true,
"force": true
}

OK.

Media typeapplication/json
object
artwork
required
One of:

UI-P7: an app’s artwork provenance and the two locally served crops.

object
app_id
required
string format: uuid
source
required

‘provider’ = matched and fetched automatically. ‘manual’ = an admin picked, supplied or uploaded it. ‘none’ = we looked and there is nothing - a NEGATIVE CACHE and a first-class outcome, not an error: a desktop app is not in a games database and never will be, so recording it stops every sweep re-querying a third party for a row that can never match. All three render correctly; ‘none’ is the gradient tile.

string
Allowed values: provider manual none
provider
required

Which provider produced the match (‘steamgriddb’); empty for a manual or unmatched app.

string
provider_ref
required

The provider’s opaque id for the matched title.

string
matched_name
required

The provider-side title that was matched. Surfaced so an operator can SEE that “Portal” matched “Portal Knights” before deciding to override it.

string
tile_url
required

Local /v1/artwork/… path for the 2:3 portrait tile crop, or null.

string | null
hero_url
required

Local /v1/artwork/… path for the wide hero crop, or null.

string | null
attribution
required

Credit line to render beside the art, when the source asks for one.

string
locked
required

True once an admin has overridden the match. The automatic sweep NEVER touches a locked record - fuzzy matching is wrong sometimes, and a correction must not be silently re-broken.

boolean
updated_at
required
string format: date-time
provider_configured
required

Whether a third-party artwork provider is configured on this deployment. False is the SHIPPED DEFAULT. The admin UI reads this to explain why the provider-backed controls are unavailable, rather than offering a button that silently does nothing. Resolved PER REQUEST from the encrypted secrets store, so a key set from the admin UI flips this without a control-plane restart.

boolean
provider_name
required

Empty when no provider is configured.

string
provider_origin
required

Where the credential in effect came from: ‘database’ (an admin set it through /v1/admin/secrets), ‘environment’ (the legacy QUASAR_STEAMGRIDDB_API_KEY), ‘static’ (a provider supplied directly at construction - embedded/test wiring only, never the shipped server) or ‘none’. An operator upgrading a deployment that already had the env var must be able to SEE that it is still what is being used.

string
Allowed values: none database environment static
provider_problem

Why the provider is unavailable despite something being configured - e.g. a stored API key the master key cannot decrypt. Empty when there is nothing to explain. Never contains any part of a credential.

string
Example
{
"artwork": {
"source": "provider"
},
"provider_origin": "none"
}

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

Conflict (duplicate, in-use, quota, non-swappable, home-in-use, …).

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