// OPENBOX DOCS
API library and settings
Library, settings, profiles, running sessions, history, launch, and CRUD routes.
Read and write the library, settings, profiles, launch configuration, and game records. All routes require X-OpenBox-Token: TOKEN; all POST bodies are JSON objects.
Library and state
/api/libraryReturns the public library projection, cached until the library file, media epoch, or plugin epoch changes. Supports pagination via ?offset=<int>&limit=<int>. Served gzip-compressed to clients that accept it with an ETag for conditional GET (If-None-Match returns 304 Not Modified). Top-level keys:
games: array of game objects (or page slice whenlimitis specified).total_count,offset,limit: present when paginated.playlists,filter_presets,ra_configured,settings,discovery,media_epoch.
In safe mode the library hook is skipped; otherwise plugin library hooks can transform the games list before it is returned.
/api/library/deltaIncremental sync endpoint returning updated projections for a specific subset of games. Accepts ?ids=id1,id2,... (up to 1,000 game IDs).
Returns:
{
"games": [ ... ],
"media_epoch": 12
}
/api/gameCreate or update one game. Locate by top-level id (numeric) or game_id (stable) for updates; omit both to create. The game properties must be nested under "game": {"id": int (opt), "game_id": str (opt), "game": {"name": "...", "path": "...", ...}}. Validation:
namerequired and non-empty inside"game".pathmust be an absolute path to an existing regular file; symlinked components are rejected and the stored path is the expanded absolute form.progressmust be one of"",Playing,Paused,Beaten,Completed,Mastered,Abandoned.ratingis a float between 0 and 5.disc_countis a non-negative integer.save_pathscapped at 50 entries,screenshotsat 100,alternate_namesat 20 (string input splits on;).applications,versions,documentsare lists capped at 100 entries each; extras need apath, get aname(defaults to the path stem), and applications/versions may carry acommand.custom_fieldsare normalized against the defined custom field defs.
Returns {"ok": true}.
/api/game/deleteRemove a game by id or game_id. With delete_media: true, associated media files (cover, background, screenshots, and all media fields) are removed only when they are safe files inside the data directory; media deletions bump the media epoch. Returns {"removed": "<name>", "deleted_media": [...], "shared_media": [...]}.
/api/games/delete-steamRemove every game with source equal to Steam (case-insensitive). Returns {"removed": <count>}.
/api/games/bulkApply the same change to many games. Body: {"ids": [...], "changes": {...}}. Allowed change keys include platform, genre, progress, rating, favorite, hidden, esrb, custom_fields, tags, tags_add, tags_remove, play_count, playtime_seconds, last_played, and reset_stats. Rules:
idsaccept numeric indexes or stable game ids, including legacy aliases.tagscannot be combined withtags_add/tags_remove; a tag cannot be added and removed in the same request.favorite/hidden/reset_statsmust be booleans;play_countandplaytime_secondsmust be non-negative integers;progressmust be a known value;ratingbetween 0 and 5.reset_stats: trueclearsplay_count,playtime_seconds, andlast_playedon each selected game. Returns{"updated": <count>}.
/api/games/bulk-wizardSame as bulk edit but validates against the bulk wizard fields (platform, genre, progress, rating, favorite, hidden, esrb, custom_fields). The wizard route is for guided edits and does not accept reset_stats, play counts, raw tags, or arbitrary fields. Returns {"updated": <count>, "fields": [...]}.
/api/favoriteToggle a game's favorite flag by id or game_id. Returns {"favorite": <bool>}.
Settings
/api/settingsReturns the public settings projection: every validated setting key with defaults, plus save_tools (ludusavi/hoard presence), safe_mode, version, appimage, gamescope_guest, and premium_features_free: true. Secrets are never included; credential presence is exposed as booleans (gameyfin_password_set, emumovies_configured).
/api/settingsPartial save: merge the posted keys into existing settings and validate. See Configuration for the full validation table. Empty gameyfin_password keeps the stored password. Returns the new public settings.
Profiles
/api/profilesReturns {"profiles": state.profiles, "detected": discover_profiles()} where detected lists launchable platform defaults from binaries on PATH (DOSBox, Wine, MAME, Dolphin, PCSX2, PPSSPP, RPCS3, DuckStation).
/api/profilesReplace all profiles: {"profiles": {"Platform": "command {path}", ...}} (blank entries dropped). Returns {"saved": <count>}.
/api/perf_profilesReads per-launch-profile TDP limits for handheld tuning. Each entry: {"enabled": bool, "tdp_w": float, "restore_tdp_w": float}.
/api/perf_profilesSaves per-launch-profile TDP limits for handheld tuning. Each entry: {"enabled": bool, "tdp_w": float, "restore_tdp_w": float}; blank names and all-zero entries are dropped; non-numeric TDP raises 400. Returns {"saved": <count>}.
Running sessions and history
/api/running{"running": [...], "abandoned": [...], "events": [...], "last_event": <sequence>}. Poll with ?after=<sequence> to receive only new session lifecycle events (id, kind started|stopped|paused|resumed, launch_id, game, time, optional exit_code/seconds). In-memory event buffer holds the last 100 events.
/api/session/control{"launch_id": "...", "action": "pause"|"resume"|"stop"|"restart"|"kill"}. stop/restart send SIGTERM to the process group, kill sends SIGKILL; paused games are resumed first except for kill. A game that is no longer running raises 400. Returns {"ok": true, "action": action}.
/api/session/cleanupRemoves an abandoned persisted active-session row by launch_id; it does not signal a running process. Returns {"ok": true}.
/api/history{"history": [...], "enabled": <track_session_history>}. Sessions are newest first; ?limit= clamps to 1..500 (default 100). Each session: game, started, seconds, exit_code.
/api/v2/history/timelineThe session history as a chronological timeline rather than a newest-first list, for the Play History view. Read-only; the same track_session_history toggle applies.
Launch
/api/launchStart a game. Body: {"id": <index>} or {"game_id": "<stable>"} (or both). Resolution prefers game_id; a missing game raises 400. Before launch: archive extraction (if configured), performance profile apply, then before_launch plugins (unless safe mode) which may rewrite args/cwd or cancel the launch with an error. The process starts in a new session; the response returns the running-session entry (launch_id, pid, game, game_path, started, and storefront ids). Launch validation errors include missing path, nonexistent path, non-executable file without a command, and plugin failures.
/api/extra/launchLaunch an application/version/document extra: {"id"|"game_id", "kind": "applications"|"versions"|"documents", "index": <n>}. Documents open with xdg-open; extras with a command substitute {path}; others execute directly. Missing file or xdg-open raises 400.
Related games
/api/related?id=<index> returns {"ids": [indexes]} scored from local metadata only (genre overlap, series, collection, developer, platform, publisher). Unknown game returns 404.
/api/related/rich?id=<index> returns {"items": [{"id", "score", "reasons": [...]}]} with human-readable reasons. 404 when the game is missing.
Per-game v2 writes
These POST routes exist alongside the frozen v1 game-update route. Each resolves exactly one game from the payload via game_from_payload, which prefers a stable game_id (or stable_game_id, storefront ids, or path/name) and otherwise falls back to a numeric id index; a game that cannot be resolved returns 400. None of them accept a bulk game_ids list. Each returns a small acknowledgement, not the public game object: progress/set returns {"ok": true, "game_id", "progress"}, rating/set returns {"ok": true, "game_id", "user_rating"}, playtime/log and playtime/update return {"ok": true, "game_id", "entry"}, and notes/add, notes/update, notes/delete, and playtime/delete return {"ok": true, "game_id"}.
/api/v2/library/progress/setSets the progress status of a game ("" to clear, otherwise a known progress value). Invalid values return 400.
/api/v2/library/rating/setSets the personal user_rating star rating for a game, an integer 0 to 5 where 0 clears it. This is separate from the metadata rating float, which this route does not touch. A missing, non-numeric, or out-of-range value returns 400 ("User rating must be an integer from 0 to 5.").
/api/v2/library/notes/addAppends a dated note to a game's notes. Body: text (required, non-blank, max 2000 characters); blank or oversized text returns 400. The handler stamps ts itself and returns {"ok": true, "game_id"} — it does not echo the saved note. Legacy plain-string notes migrate to {"ts", "text"} entries on the first mutation.
/api/v2/library/notes/updateReplaces an existing note's text, addressed by its positional index in the game's notes list — notes have no id. Body: index (integer) and text (required, non-blank, max 2000 characters). A non-integer index returns 400 ("index must be an integer."), and so does an index outside the list ("Unknown note entry.") — the route never returns 404. The entry keeps its original ts.
/api/v2/library/notes/deleteDeletes the note at positional index in the game's notes list. A non-integer or out-of-range index returns 400, never 404.
/api/v2/library/playtime/logAdds a manual playtime session for a game without running a tracked session. Body: seconds (required integer, 1-86400); optional date (a YYYY-MM-DD string, "" when omitted) and note (max 200 characters). There is no minutes field — a missing, non-integer, or out-of-range seconds returns 400 ("seconds must be an integer." / "seconds must be between 1 and 86400."), as does a malformed date ("date must be YYYY-MM-DD.") or an oversized note. The entry counts toward manual_playtime_seconds, and the response is {"ok": true, "game_id", "entry"}.
/api/v2/library/playtime/updateReplaces the manual playtime entry at positional index in the game's session list. Body: index (integer) plus a full replacement entry validated exactly as for playtime/log (seconds required 1-86400, optional date, optional note). A non-integer or out-of-range index returns 400, never 404.
/api/v2/library/playtime/deleteRemoves the manual playtime entry at positional index in the game's session list, then recomputes manual_playtime_seconds from the remaining entries. A non-integer or out-of-range index returns 400, never 404.
Library health
The scheduled health audit and its repair flow. health_rescan in Settings chooses the cadence (daily, weekly, on_startup, off).
/api/v2/library/healthThe latest library-health snapshot: issue counts by severity, missing media, orphaned saves, broken paths, and the timestamp of the last scan.
/api/v2/library/health/issuesThe individual health issues behind the snapshot, filterable and paged.
/api/v2/library/health/scanQueues a health scan as a background job. Returns 202 with a job_id you can track through GET /api/v2/jobs.
/api/v2/library/health/fixApplies a previously previewed repair. Requires the base_token returned by the preview so a concurrent library change fails instead of repairing a stale view.
/api/v2/library/health/undoReverts the most recent health repair using its recorded undo token.
Duplicate repair
/api/v2/library/duplicatesLists detected duplicate groups with their member records.
/api/v2/library/duplicates/previewPreviews a merge for a duplicate group: which record survives, which fields are kept, and which entries are removed. Nothing is written.
/api/v2/library/duplicates/mergeApplies a previewed merge. Requires the preview's base_token; a stale token returns 409 rather than merging against changed data.
Structural repair
/api/v2/library/repairLists repairable structural problems found in library records (malformed entries, missing platform metadata, stale references).
/api/v2/library/repair/previewPreviews the field-level changes a repair would make, pinned to the current state token.
/api/v2/library/repair/applyApplies a previewed repair. A missing or stale base_token returns 409.
Library DNA
/api/v2/library/dna/statusReports the DNA index state: whether an index exists, how many games it covers, and when it was last rebuilt.
/api/v2/library/dna/searchRuns a DNA-style similarity search over indexed artwork/metadata and returns ranked candidates.
/api/v2/library/dna/index/rebuildQueues a full DNA index rebuild as a background job. Returns 202 with a job_id.
Playlists
/api/playlistsSave a playlist: {"name", "type": "filter"|"manual", "rules": {...}, "members"|"ids": [...], "parent", "notes"}. Filter playlists keep rules and drop members; manual playlists store stable game_ids in order (deduplicated, at most 100,000). Updating an existing name replaces it. Returns {"saved": "<name>"}.
/api/playlists/delete{"name": "..."} removes the playlist. Returns {"deleted": "<name>"}. Deleting a playlist that does not exist returns 404 "Playlist not found: <name>" instead of a false success. Scoped library exports against an unknown playlist fail with the same "Playlist not found: <name>" string (as the export job error).
Filter presets
/api/filter-presets{"presets": [...], "bigbox_quick": [...]} (at most 8 quick presets).
/api/filter-presets{"name", "rules", "bigbox_quick": bool}. Rules keys: platform, view, query, esrb, progress, favorite, installed, platform_category, genre, developer, publisher, hidden, has_saves, has_achievements, has_missing_media, and has_highscores. At least one rule required. Returns {"saved": "<name>"}.
/api/filter-presets/delete{"name"} removes it; unknown names raise 400.
Image groups
/api/image-group{"group": "default"|"cover"|"background"|"screenshot"|"clear_logo"|"fanart"|"banner"|"icon"|"box_back"|"box_spine"|"box_3d"|"title_screen"|"cart_front"|"cart_back"|"disc"|"advertisement"|"manual", "scope": "global"|"platform"|"playlist", "name": "<platform or playlist>"}. Global sets image_group; scoped sets/clears image_group_by_platform or image_group_by_playlist. Returns updated settings.
Errors
All handlers return {"error": "..."}; unknown games, bad payloads, and validation failures surface as 400, missing game lookups as 404, corrupt state as 503. See REST API overview for the shared envelope.