// 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

GET/api/library

Returns 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 when limit is 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.

GET/api/library/delta

Incremental 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
}
POST/api/game

Create 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:

  • name required and non-empty inside "game".
  • path must be an absolute path to an existing regular file; symlinked components are rejected and the stored path is the expanded absolute form.
  • progress must be one of "", Playing, Paused, Beaten, Completed, Mastered, Abandoned.
  • rating is a float between 0 and 5.
  • disc_count is a non-negative integer.
  • save_paths capped at 50 entries, screenshots at 100, alternate_names at 20 (string input splits on ;).
  • applications, versions, documents are lists capped at 100 entries each; extras need a path, get a name (defaults to the path stem), and applications/versions may carry a command.
  • custom_fields are normalized against the defined custom field defs.

Returns {"ok": true}.

POST/api/game/delete

Remove 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": [...]}.

POST/api/games/delete-steam

Remove every game with source equal to Steam (case-insensitive). Returns {"removed": <count>}.

POST/api/games/bulk

Apply 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:

  • ids accept numeric indexes or stable game ids, including legacy aliases.
  • tags cannot be combined with tags_add/tags_remove; a tag cannot be added and removed in the same request.
  • favorite/hidden/reset_stats must be booleans; play_count and playtime_seconds must be non-negative integers; progress must be a known value; rating between 0 and 5.
  • reset_stats: true clears play_count, playtime_seconds, and last_played on each selected game. Returns {"updated": <count>}.
POST/api/games/bulk-wizard

Same 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": [...]}.

POST/api/favorite

Toggle a game's favorite flag by id or game_id. Returns {"favorite": <bool>}.

Settings

GET/api/settings

Returns 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).

POST/api/settings

Partial 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

GET/api/profiles

Returns {"profiles": state.profiles, "detected": discover_profiles()} where detected lists launchable platform defaults from binaries on PATH (DOSBox, Wine, MAME, Dolphin, PCSX2, PPSSPP, RPCS3, DuckStation).

POST/api/profiles

Replace all profiles: {"profiles": {"Platform": "command {path}", ...}} (blank entries dropped). Returns {"saved": <count>}.

GET/api/perf_profiles

Reads per-launch-profile TDP limits for handheld tuning. Each entry: {"enabled": bool, "tdp_w": float, "restore_tdp_w": float}.

POST/api/perf_profiles

Saves 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

GET/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.

POST/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}.

POST/api/session/cleanup

Removes an abandoned persisted active-session row by launch_id; it does not signal a running process. Returns {"ok": true}.

GET/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.

GET/api/v2/history/timeline

The 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

POST/api/launch

Start 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.

POST/api/extra/launch

Launch 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.

GET/api/related

?id=<index> returns {"ids": [indexes]} scored from local metadata only (genre overlap, series, collection, developer, platform, publisher). Unknown game returns 404.

GET/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"}.

POST/api/v2/library/progress/set

Sets the progress status of a game ("" to clear, otherwise a known progress value). Invalid values return 400.

POST/api/v2/library/rating/set

Sets 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.").

POST/api/v2/library/notes/add

Appends 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.

POST/api/v2/library/notes/update

Replaces 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.

POST/api/v2/library/notes/delete

Deletes the note at positional index in the game's notes list. A non-integer or out-of-range index returns 400, never 404.

POST/api/v2/library/playtime/log

Adds 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"}.

POST/api/v2/library/playtime/update

Replaces 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.

POST/api/v2/library/playtime/delete

Removes 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).

GET/api/v2/library/health

The latest library-health snapshot: issue counts by severity, missing media, orphaned saves, broken paths, and the timestamp of the last scan.

GET/api/v2/library/health/issues

The individual health issues behind the snapshot, filterable and paged.

POST/api/v2/library/health/scan

Queues a health scan as a background job. Returns 202 with a job_id you can track through GET /api/v2/jobs.

POST/api/v2/library/health/fix

Applies a previously previewed repair. Requires the base_token returned by the preview so a concurrent library change fails instead of repairing a stale view.

POST/api/v2/library/health/undo

Reverts the most recent health repair using its recorded undo token.

Duplicate repair

GET/api/v2/library/duplicates

Lists detected duplicate groups with their member records.

POST/api/v2/library/duplicates/preview

Previews a merge for a duplicate group: which record survives, which fields are kept, and which entries are removed. Nothing is written.

POST/api/v2/library/duplicates/merge

Applies a previewed merge. Requires the preview's base_token; a stale token returns 409 rather than merging against changed data.

Structural repair

GET/api/v2/library/repair

Lists repairable structural problems found in library records (malformed entries, missing platform metadata, stale references).

POST/api/v2/library/repair/preview

Previews the field-level changes a repair would make, pinned to the current state token.

POST/api/v2/library/repair/apply

Applies a previewed repair. A missing or stale base_token returns 409.

Library DNA

GET/api/v2/library/dna/status

Reports the DNA index state: whether an index exists, how many games it covers, and when it was last rebuilt.

POST/api/v2/library/dna/search

Runs a DNA-style similarity search over indexed artwork/metadata and returns ranked candidates.

POST/api/v2/library/dna/index/rebuild

Queues a full DNA index rebuild as a background job. Returns 202 with a job_id.

Playlists

POST/api/playlists

Save 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>"}.

POST/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

GET/api/filter-presets

{"presets": [...], "bigbox_quick": [...]} (at most 8 quick presets).

POST/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>"}.

POST/api/filter-presets/delete

{"name"} removes it; unknown names raise 400.

Image groups

POST/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.