// OPENBOX DOCS

API 1.11 additions

Request and response guidance for the local-first v2 workflows shipped in OpenBox 1.11.0.

OpenBox 1.11.0 adds these authenticated, additive /api/v2/* workflows without changing the frozen v1 contract. The server still binds to loopback, chooses a random port at launch, and accepts X-OpenBox-Token: TOKEN on every protected route.

DATA_DIR="$HOME/.local/share/openbox-game-launcher"
TOKEN=$(cat "$DATA_DIR/server.token")
PORT=$(cat "$DATA_DIR/server.port")
BASE="http://127.0.0.1:$PORT"

Use the header form in scripts. The query-string token is accepted for the browser launch path but can leak into history and logs. POST bodies are JSON objects and are subject to the shared 65,536-byte body limit. Durable operations return 202 with a job_id; inspect them through the jobs endpoints documented in Saves and operations. On Windows the data directory is %LOCALAPPDATA%\openbox-game-launcher, so server.token and server.port live there instead.

Quick Resume and Moments

Quick Resume reports adapter capability and state freshness before loading a saved emulator state. Pass allow_stale: true only when you have reviewed the adapter or emulator-version mismatch.

MethodRoutePurpose
GET/api/v2/resume/status?game_id=GAME_IDReport whether the game is enabled, capable, available, or stale.
POST/api/v2/resumeResume a game from its captured state. Body: game_id, optional allow_stale.
POST/api/v2/resume/discardDelete the captured state and metadata. Body: game_id.

Moments are bounded timeline records attached to one game. A capture can include a screenshot when the host supports it, but the note and trigger remain durable when capture is unavailable.

MethodRoutePurpose
GET/api/v2/moments?game_id=GAME_ID&limit=100List a game's newest moments. limit is 1–500.
GET/api/v2/moments?moment_id=MOMENT_IDFetch one moment and its game id.
POST/api/v2/momentsCreate a moment. Body: game_id, optional title, note, and trigger.
POST/api/v2/moments/resumeLaunch the exact state linked to a moment. Body: moment_id, optional game_id.
POST/api/v2/moments/updateChange title and/or note. Body: moment_id, optional game_id.
POST/api/v2/moments/deleteRemove a timeline item. Body: moment_id, optional game_id.
GET/api/v2/sessions/recapReturn the latest local session recap when recap tracking is enabled.

Example:

curl -s -X POST "$BASE/api/v2/moments" \
  -H "X-OpenBox-Token: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"game_id":"GAME_ID","title":"Boss attempt","note":"Try the left route next time","trigger":"manual"}'

Record That: clips and reels

The clip capture route uses the configured OBS replay buffer when enabled and falls back to a local screenshot clip when OBS is unavailable. Returned clip paths are approved local media paths, not arbitrary file reads.

MethodRoutePurpose
GET/api/v2/clips?game_id=GAME_IDList the bounded clip collection for a game.
POST/api/v2/clips/captureCapture a replay or screenshot fallback. Body: game_id, optional launch_id.
GET/api/v2/reels?game_id=GAME_IDBuild a deterministic reel manifest from the game's clips and moments.
POST/api/v2/reels/createQueue local reel creation. Body: game_id, optional year.

Time Machine and query grammar

Time Machine is journal-backed and bounded. Reads do not mutate the library. Reverts are preview-first: the response contains a plan and base_token; send that reviewed plan back with apply: true and the current base token. A stale plan returns 409 and must be previewed again.

MethodRoutePurpose
GET/api/v2/library/time-machine/eventsPage journal events. Optional query: days, kind, game_id, offset, limit.
GET/api/v2/library/time-machine/as-of?date=YYYY-MM-DDMaterialize the bounded library view at a date.
POST/api/v2/library/time-machine/revertPreview or apply a revert. Body: event_id, optional game_id, fields, undo; add apply: true and the returned base_token to apply.
GET/api/v2/library/time-machine/compare?after=YYYY-MM-DD&before=YYYY-MM-DDRead-only diff of the library at two journal dates. after is required; omitting before diffs against an empty baseline, so every game reads as added (a restore-point listing) and removed/changed are zero. Optional fields (comma-separated) bounds which catalog fields are compared; limit pages each list. Requires the library journal. Reversed dates return 400 TM_INVALID_DATE. (v1.15.0+)
POST/api/v2/library/query/parseParse a natural-language query into deterministic filters without changing library state.
GET/api/v2/library/trashList soft-deleted library entries.
POST/api/v2/library/trashSoft-delete a reviewed library entry. Body: game_id.
POST/api/v2/library/trash/restoreRestore a reviewed trash entry.
POST/api/v2/library/trash/purgePermanently remove a trash entry. Treat this as destructive.

Preview a revert before applying it:

curl -s -X POST "$BASE/api/v2/library/time-machine/revert" \
  -H "X-OpenBox-Token: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"event_id":"EVENT_ID"}'

Backlog Radio and launcher trophies

Backlog Radio and Radar use local library and history data. They do not call a remote recommendation service.

MethodRoutePurpose
GET/api/v2/insights/radioRead or lazily refresh the managed recommendation playlist.
POST/api/v2/insights/radio/refreshForce a fresh local recommendation set.
GET/api/v2/insights/radarReturn progress-oriented backlog candidates.
POST/api/v2/insights/radar/parkPark one candidate. Body: game_id.
GET/api/v2/insights/trophiesReturn the local trophy case and current rule status.
POST/api/v2/insights/trophies/evaluateEvaluate and persist newly earned local trophies.

Launcher trophies are deterministic OpenBox milestones over local library and history data. They are not RetroAchievements, do not require an account, and do not submit data to a service.

Each radio pick and radar entry carries estimated_minutes (integer, rounded): the genre-based length estimate minus already-played minutes (floor 15; unplayed games report the full estimate). Picks hydrated from the stored managed playlist carry it too, with a frontend fallback for older entries. The genre fallback when no history exists is RPG/strategy/simulation 120 min, adventure/action/shooter/platform/fighting 60 min, puzzle/card/board 30 min, and 45 min otherwise; a game fits a requested session when its median session (or the fallback estimate) is at most 1.5x the requested minutes.

GET /api/v2/insights/wrapped returns the year-in-review "wrapped" payload assembled from the session journal, playtime, and completion data. It is a pure read projection: nothing is written, so the summary can never go stale.

year is a required query parameter naming the calendar year to summarize. Omitting it returns 400 ("year is required"); a non-integer returns 400 ("year must be an integer"), and a year outside 1970-2100 returns 400 ("year must be between 1970 and 2100"). Example: /api/v2/insights/wrapped?year=2026.

Game Night party queue

Game Night builds a couch-multiplayer queue from the local library. Since 1.12.1 an empty build explains itself instead of returning a bare empty list.

MethodRoutePurpose
POST/api/v2/party/queueBuild a queue. Body: players (2–8), optional minutes session budget. Returns queue, count, empty_reason, excluded.
GET/api/v2/party/queueRead the persisted queue and round index.
POST/api/v2/party/nextAdvance the round. An empty queue returns 400.
{
  "queue": [],
  "count": 0,
  "empty_reason": "No games support 6 players — lower the player count or add Max players metadata.",
  "excluded": {"total": 120, "hidden": 4, "unusable_path": 10, "too_few_players": 96, "no_controller_or_platform": 8, "over_budget": 2}
}

empty_reason is null when the queue is non-empty; the top exclusion reason wins (empty library, too few players, no couch-ready titles, missing files, over-budget sessions, all hidden). Non-object bodies and non-integer or out-of-range players/minutes return 400.

Arcade Room and Museum kiosk

Arcade Room exposes the controller-first showroom state. The optional kiosk PIN is a convenience boundary for a local Museum presentation; it is not a replacement for the API session token or a security boundary.

MethodRoutePurpose
GET/api/v2/arcade/kiosk/statusReport whether Museum kiosk mode is enabled and whether a PIN is set.
POST/api/v2/arcade/kiosk/pinSet or clear a PIN. Body: a 4–12 digit pin, or clear: true; enabled may be boolean.
POST/api/v2/arcade/kiosk/verifyVerify a candidate PIN. Body: pin.

Household records and sync

Household is local-first. It stores members, challenges, results, and opt-in shares in the local state and can exchange validated records through the configured mounted sync folder. It does not provide hosted accounts or real-time multiplayer.

MethodRoutePurpose
GET/api/v2/household?period=all_timeReturn records, challenge progress, sharing state, and a leaderboard. Periods: daily, weekly, monthly, all_time.
GET/api/v2/household/leaderboard?period=all_timeReturn the leaderboard projection for one period.
POST/api/v2/household/memberAdd or update a member. Body: member_id, display_name, optional avatar_color, stats_shared.
POST/api/v2/household/challengeCreate a challenge. Body: title; optional challenge_id, metric, target, description, game_id, deadline, participant_ids.
POST/api/v2/household/challenge/resultRecord progress. Body: challenge_id, member_id, optional value, completed, game_id, note.
POST/api/v2/household/sharePublish an opt-in stats share. Body: member_id, optional period and precomputed stats.
POST/api/v2/household/recordAppend one validated local record. Body: record.
POST/api/v2/household/mergeMerge a validated record list. Body: records.
POST/api/v2/household/sync/publishPublish pending records to the configured folder. Optional protocol.
POST/api/v2/household/sync/pullRead and merge validated records from the configured folder. Optional protocol.

Steam Bridge and ES-DE migration

Both integrations are review-first and preserve foreign data. Preview responses include a plan or import decisions; apply only the reviewed plan against an unchanged source. A changed source returns a conflict instead of silently overwriting it. Steam Bridge reports a stale preview as HTTP 409 with STEAMBRIDGE_PREVIEW_STALE; ES-DE reports its stale-plan condition as HTTP 400 with ESDE_STALE_PLAN. In either case, preview again before applying.

MethodRoutePurpose
GET/api/v2/steambridge/statusInspect the discovered shortcuts.vdf path and OpenBox/foreign counts.
POST/api/v2/steambridge/previewBuild a Steam Bridge plan. Optional body: absolute path, game_ids, launcher_exe.
POST/api/v2/steambridge/applyApply a reviewed plan. Body: plan; path may also be supplied.
POST/api/v2/steambridge/remove/previewPreview removal of OpenBox shortcuts. Body: targets, optional path.
POST/api/v2/steambridge/removeApply a reviewed removal plan. Body: plan; path may also be supplied.
POST/api/v2/import/esde/previewParse an ES-DE gamelist.xml. Body: absolute xml_path (or gamelist), optional options.
POST/api/v2/import/esde/applyApply a reviewed ES-DE plan after a source-digest check. Body: xml_path/gamelist, plan, and optional preview_token/options.

SteamGridDB artwork

SteamGridDB is optional. Set STEAMGRIDDB_API_KEY in ~/.env, enable the provider in Settings, and restart OpenBox after changing the environment file. Search and apply use a bounded local cache; apply and bulk match return durable job IDs.

MethodRoutePurpose
GET/api/v2/steamgrid/statusReport configuration, toggle, provider state, and cache information.
GET/api/v2/steamgrid/search?q=TITLE&limit=12Search SteamGridDB games.
POST/api/v2/steamgrid/testProbe the configured provider.
POST/api/v2/steamgrid/infoFetch normalized metadata and media for a result. Body: steamgrid_id.
POST/api/v2/steamgrid/applyQueue artwork/metadata for one game. Body: stable id; optional steamgrid_id, fields, media, replace_existing.
POST/api/v2/steamgrid/matchQueue bounded bulk matching. Optional body: ids and media. Without ids, games missing a cover are selected.

The supported media kinds are cover, background, clear_logo, icon, and banner. Provider artwork is written into OpenBox-managed media paths; credentials are never returned in API responses.

Compatibility and error handling

All routes on this page are additive v2 routes. The frozen /api/v1/* surface remains unchanged. The shared API contract still applies:

  • 403 {"error":"Unauthorized"} means a known protected route received no valid token; an unknown route can resolve to 404 before auth.
  • 409 is used for stale review plans or launch/state conflicts where the operation must be previewed or retried.
  • 202 {"state":"queued","job_id":"…"} means the local durable-operation manager accepted the work; query /api/v2/jobs for progress.
  • 400 responses identify validation failures with the route's error message and, for structured errors, a stable code.
  • 503 means local state or an explicitly required integration is unavailable; no destructive mutation should be inferred from the response.

See REST API overview for the request lifecycle, REST API for group navigation, and the corresponding user guides for UI-level workflows.