// OPENBOX DOCS

REST API overview

Loopback boundary, authentication, body limits, and the request lifecycle.

The Web UI starts a ThreadingHTTPServer bound to 127.0.0.1 on a random port. This page documents the shared contract every route follows and the lifecycle a request goes through. Route lists live on the group pages.

Loopback boundary

  • The server binds to ("127.0.0.1", 0), so nothing is reachable from the network.
  • OpenBoxGL does not terminate HTTPS. Use a trusted network and an external reverse proxy if you must expose it beyond the host.
  • Requests carry a 30-second socket timeout (Handler.REQUEST_TIMEOUT).
  • The static UI is served at / and /index.html; locale JSON and the favicon are public assets. Every other API route requires authentication.

Authentication

All API routes require the session token. The server writes it to server.token in the data directory at startup and deletes the file on exit. Two transports are accepted:

  • Header: X-OpenBox-Token: TOKEN (preferred)
  • Query parameter: ?token=TOKEN (can leak through history and logs)

The check uses secrets.compare_digest. Known protected routes are checked after route resolution; an unknown route can therefore return 404 before authentication. A missing or wrong token on a known protected route returns 403 {"error":"Unauthorized"}. Repeated failures can return 429 with code: "RATE_LIMITED", retry_after, and a Retry-After header.

Request lifecycle

A request flows through a fixed set of stages. Knowing the order tells you which status code you will get and why:

GET: resolve public/static/known route
 └─ unknown route? → 404 ROUTE_NOT_FOUND
 └─ protected route with wrong/missing token? → 403 Unauthorized
 └─ dispatch to handler

POST: read body (Content-Length, ≤ 65536 bytes, valid JSON object)
 ├─ too large → 400 "Request is too large."
 ├─ truncated → 400 "Request body was truncated."
 └─ not a JSON object → 400
resolve known route
 └─ unknown route? → 404 ROUTE_NOT_FOUND
protected route with wrong/missing token? → 403 Unauthorized
dispatch to handler (routes.py → Handler.<method>)
 ├─ state corrupt? → 503 STATE_UNAVAILABLE "OpenBox library data needs recovery…"
 ├─ handler raises ApiError → its status + code (GAME_NOT_FOUND, MEDIA_NOT_FOUND, …)
 ├─ POST handler raises ValueError/OSError/… → 400 BAD_REQUEST {error: message}
 └─ any other exception → 500 INTERNAL_ERROR "Unexpected server error. Copy the diagnostic log…"
send JSON response (nosniff, CSP, Referrer-Policy: no-referrer)

Most structured error responses carry a stable code and a request_id. The request id is a short per-request token that also appears in the diagnostic log, so a screenshot of an error banner can be correlated with the log line. The direct unauthorized response is intentionally the smaller {"error":"Unauthorized"} envelope; rate-limit and outer-handler errors include their additional fields.

POST handlers re-raise ApiError unchanged and convert ValueError, OSError, TypeError, AttributeError, KeyError, IndexError, json.JSONDecodeError, GameyfinError, FileNotFoundError, RuntimeError, and subprocess.SubprocessError into 400 BAD_REQUEST with the message in error. State-corruption errors become 503 before handler dispatch. Everything else is logged and returned as 500.

This is why almost every operation failure you hit through the API is a 400 with a readable, specific message, and why a 500 is genuinely "something the developers need to see," not a normal error path.

Bodies

  • POST bodies must be JSON. Content-Length must be a valid non-negative number; bodies over 65,536 bytes are rejected with "Request is too large."; truncated bodies with "Request body was truncated.".
  • The body must decode to a JSON object; [], null, "text", and malformed JSON return 400 {"error": ...}.
  • GET requests take parameters from the query string (parse_qs); lists and repeated keys behave per parse_qs (values are lists).

Response headers

JSON responses are served with Cache-Control: no-store, X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, and a restrictive Content-Security-Policy (default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.bunny.net; font-src …; script-src 'self'; object-src 'none'; base-uri 'none'). Rate-limited auth returns 429 with Retry-After. The socket times out after 30 seconds (Handler.REQUEST_TIMEOUT), SSE events are capped at 64 KiB (SSE_MAX_EVENT_BYTES), at most 16 SSE subscribers (SSE_MAX_SUBSCRIBERS), and the webhook envelope at 64 KiB (MAX_ENVELOPE_BYTES). Media files are served with immutable cache headers, ETag, Last-Modified, Accept-Ranges, and byte-range (206/416) support; theme.css revalidates with public, max-age=0, must-revalidate and ETag. Gzip is applied when Accept-Encoding: gzip and the JSON is at least 1 KiB (GZIP_THRESHOLD=1024).

Common errors

StatusEnvelopeWhen
403{"error":"Unauthorized"}Missing or wrong token on any known protected route, including /api/native/*
403{"error":"Request host is not allowed.","code":"BAD_HOST","request_id":"…"}Host header mismatch or foreign network host rejected
429{"error":"Too many authentication failures. Try again later.","code":"RATE_LIMITED","retry_after":n}Rate limited after repeated auth failures (Retry-After header sent)
400{"error": "<message>", "code": "BAD_REQUEST", "request_id": "…"}Validation failures, missing prerequisites surfaced by handlers, provider errors, unknown queue/notification actions, malformed payloads
404{"error":"Not found","code":"ROUTE_NOT_FOUND","request_id":"…"}Unknown route after the request body has been accepted (for POST)
404GAME_NOT_FOUND / MEDIA_NOT_FOUND / DOCUMENT_NOT_FOUND / PLATFORM_DOCUMENT_NOT_FOUND / BADGE_NOT_FOUNDLookups that miss
409{"error":"Download the LaunchBox metadata database first."}Metadata routes before the database exists
503{"error":"OpenBox library data needs recovery before this operation can continue.","code":"STATE_UNAVAILABLE"}Corrupt state file
500{"error":"Unexpected server error. Copy the diagnostic log from Settings and include it in your report.","code":"INTERNAL_ERROR"}Unhandled exception

Versioned surface

  • Frozen v1 Contract (/api/v1/*): Stable, frozen remote contract (enforced via v1_contracts.json, currently 61 routes; the contract pins path + methods, per-route params are manually verified).
  • Base-only routes (/api/* without a /api/v1/* alias): local-workflow routes such as GET /api/emulators/recommend, GET /api/plugins/catalog, POST /api/webhooks/test, POST /api/plugins/install|toggle|remove, /api/wine/*, and /api/faugus/* are documented on the group pages as /api/* with no v1 alias. Treat them as local operations, not part of the frozen remote contract; v1 aliases are added only when a route becomes a stable remote dependency.
  • Additive v2 API (/api/v2/*): Modern workflows introduced in v1.7.0 through v1.14.0:
    • Setup Center: POST /api/v2/setup/preview, GET /api/v2/setup/preview, GET /api/v2/setup/preview/items, POST /api/v2/setup/preview/decisions, POST /api/v2/setup/preview/revalidate, POST /api/v2/setup/commit, GET /api/v2/setup/summary
    • Launch Doctor: POST /api/v2/launch/preflight, POST /api/v2/launch/preflight/batch
    • Emulator Registry: GET /api/v2/emulators/registry (with ?health=1 for BIOS/firmware/core status)
    • Metadata Matches: POST /api/v2/metadata/matches/preview, GET /api/v2/metadata/matches/preview, GET /api/v2/metadata/matches/items, POST /api/v2/metadata/matches/decisions, POST /api/v2/metadata/matches/apply
    • Durable Operations: GET /api/v2/jobs, GET /api/v2/jobs/items, POST /api/v2/jobs/cancel, POST /api/v2/jobs/retry, POST /api/v2/jobs/resume
    • Play Insights: GET /api/v2/insights/summary, GET /api/v2/insights/heatmap
    • Backup Diff: GET /api/v2/backup/diff?archive=<name> (v1.7.2)
    • ScreenScraper: GET /api/v2/screenscraper/status, GET /api/v2/screenscraper/search, POST /api/v2/screenscraper/test, POST /api/v2/screenscraper/info, POST /api/v2/screenscraper/match (batch, cancellable), POST /api/v2/screenscraper/apply (durable job that downloads media outside the state lock) (v1.8.0)
    • Library Export: POST /api/v2/library/export (queues durable job), GET /api/v2/library/export/exports, GET /api/v2/library/export/download (v1.8.0)
    • Picker (v1.9.0): POST /api/v2/library/pick (scored recommendations by time, mood, familiarity, players)
    • Constellation (v1.9.0): GET /api/v2/library/constellation (capped deterministic nodes/edges)
    • Wrapped + Timeline + Mastery (v1.9.0): GET /api/v2/insights/wrapped?year=YYYY, GET /api/v2/history/timeline?days=90, GET /api/v2/insights/mastery
    • Game Night (v1.9.0): POST /api/v2/party/queue, GET /api/v2/party/queue, POST /api/v2/party/next
    • LaunchBox Migration (v1.10.0): POST /api/v2/import/launchbox/preview, POST /api/v2/import/launchbox/apply
    • Manual Entries (v1.10.0): POST /api/v2/library/manual-entry, POST /api/v2/library/manual-entry/update, POST /api/v2/library/manual-entry/convert
    • Causal Catalog Sync (v1.10.0): POST /api/v2/library/sync/preview, POST /api/v2/library/sync/apply, and POST /api/v2/library/sync/publish with {"protocol":"v3"}. Preview is read-only; apply requires the reviewed plan and creates a recovery backup; publish acknowledges the local outbox after a stale-state check. The old full-library publish payload and POST /api/v2/library/sync/pull return 503 LIBRARY_SYNC_UNAVAILABLE before mutation.
    • SQLite Search (v1.9.0): GET /api/v2/library/search?q=&limit= (FTS5 or LIKE fallback; OPENBOX_ENABLE_SQLITE_READ=1 opts in, and the read model self-enables at 5,000+ games since v1.12.0 unless opted out)
    • Quick Resume, Moments, and session context (v1.11.0): GET /api/v2/resume/status, POST /api/v2/resume, POST /api/v2/resume/discard, GET /api/v2/moments, POST /api/v2/moments, POST /api/v2/moments/resume, POST /api/v2/moments/update, POST /api/v2/moments/delete, GET /api/v2/sessions/recap, GET /api/v2/clips, POST /api/v2/clips/capture, GET /api/v2/reels, and POST /api/v2/reels/create. Resume is adapter-capability and state-fingerprint aware; clips use an optional OBS replay buffer with a local screenshot fallback; reel creation is a durable local operation.
    • Memory gallery (v1.11.0): GET /api/v2/memories, GET /api/v2/memories/status, GET /api/v2/memories/media, and POST /api/v2/memories/import. Import is opt-in and queued; the read routes never scan configured folders themselves.
    • Time Machine and query grammar (v1.11.0): GET /api/v2/library/time-machine/events, GET /api/v2/library/time-machine/as-of, POST /api/v2/library/time-machine/revert, and POST /api/v2/library/query/parse. Time Machine is journal-backed and bounded; revert previews must be reviewed and applied with a current base_token. Query parsing is read-only and deterministic.
    • Backlog Radio (v1.11.0): GET /api/v2/insights/radio, POST /api/v2/insights/radio/refresh, GET /api/v2/insights/radar, and POST /api/v2/insights/radar/park. Recommendations use local history and return explanations; there is no remote recommendation service.
    • Arcade Room kiosk boundary (v1.11.0): GET /api/v2/arcade/kiosk/status, POST /api/v2/arcade/kiosk/pin, and POST /api/v2/arcade/kiosk/verify. The salted PIN is a local Museum convenience boundary, not API authentication or security.
    • Household (v1.11.0): GET /api/v2/household, GET /api/v2/household/leaderboard, POST /api/v2/household/member, POST /api/v2/household/challenge, POST /api/v2/household/challenge/result, POST /api/v2/household/share, POST /api/v2/household/merge, POST /api/v2/household/record, POST /api/v2/household/sync/publish, and POST /api/v2/household/sync/pull. Records are local-first and sync only through a configured mounted folder; stats sharing is opt-in.
    • Steam Bridge and ES-DE migration (v1.11.0): GET /api/v2/steambridge/status, POST /api/v2/steambridge/preview, POST /api/v2/steambridge/apply, POST /api/v2/steambridge/remove/preview, POST /api/v2/steambridge/remove, POST /api/v2/import/esde/preview, and POST /api/v2/import/esde/apply. Both workflows are preview/apply based and reject stale source or reviewed plans before mutation.
    • SteamGridDB artwork (v1.11.0): GET /api/v2/steamgrid/status, GET /api/v2/steamgrid/search, POST /api/v2/steamgrid/test, POST /api/v2/steamgrid/info, POST /api/v2/steamgrid/apply, and POST /api/v2/steamgrid/match. The provider requires STEAMGRIDDB_API_KEY, is optional, and returns durable job IDs for apply and bulk match.
    • OpenBox launcher trophies (v1.11.0): GET /api/v2/insights/trophies and POST /api/v2/insights/trophies/evaluate. Awards are deterministic over local library/history data and are separate from RetroAchievements.
    • Smart collections (v1.12.0): GET /api/v2/collections, POST /api/v2/collections, and POST /api/v2/collections/delete. A collection stores the query, not a snapshot, so membership re-evaluates at read time.
    • Game Story (v1.12.0): GET /api/v2/story?game_id= — a read-only deterministic timeline per game over the game record, session journal, and Moments.
    • Per-game launch options and scheduled backups (v1.12.0): launch_env and launch_confirm fields on the game record, plus the opt-in weekly automatic backup settings backup_auto_enabled/backup_auto_keep/last_auto_backup.
    • Game DNA search (v1.14.0): GET /api/v2/library/dna/status, POST /api/v2/library/dna/search (BM25 + 151-concept lexicon, title fallback when degraded), and POST /api/v2/library/dna/index/rebuild (durable job).
    • Library health score (v1.14.0): GET /api/v2/library/health, POST /api/v2/library/health/scan, GET /api/v2/library/health/issues, POST /api/v2/library/health/fix (preview-then-execute with a preview token; the artwork dimension is the Artwork Doctor), and POST /api/v2/library/health/undo (journal-backed undo).
    • Backlog management (v1.14.0): POST /api/v2/library/progress/set, POST /api/v2/library/rating/set, POST /api/v2/library/notes/add|update|delete, and POST /api/v2/library/playtime/log|update|delete.
    • Duplicate merge and missing-file repair (v1.14.0): GET /api/v2/library/duplicates, POST /api/v2/library/duplicates/preview|merge, GET /api/v2/library/repair, and POST /api/v2/library/repair/preview|apply. Both are preview/apply based and reversible.
    • Effortless metadata (v1.14.0): GET /api/v2/metadata/media-candidates (thumbnail chooser), GET/POST /api/v2/metadata/scrape-settings (auto-scrape master toggle plus provider opt-ins), and POST /api/v2/metadata/auto-scrape (queues match + media jobs for an import batch).
    • Plugins 2.0 (v1.14.0): GET /api/v2/plugins/catalog, GET /api/v2/plugins/commands, POST /api/v2/plugins/command, GET/POST /api/v2/plugins/trust (checksum-bound), POST /api/v2/plugins/permissions, and GET/POST /api/v2/plugins/settings.

See API 1.11 additions, API 1.12 additions, and API 1.14 additions for request bodies, response examples, optional-tool caveats, and stale-plan handling.

  • Locale files (public, no auth): GET /locales/{en,es,de,fr,pt}.json — serves JSON locale files for the i18n system (v1.7.2)

Machine-Readable Error Codes

CodeMeaning
PREVIEW_NOT_FOUND / PREVIEW_STALE / PREVIEW_EXPIRED / PREVIEW_LIBRARY_CHANGEDSetup preview session expired, stale, or invalidated by a library change
PREVIEW_LIMIT_EXCEEDED / PREVIEW_ENTRY_LIMIT_EXCEEDEDPreview exceeds the entry or payload limit
UNRESOLVED_CANDIDATESCommit attempted while ambiguous candidates remain unresolved
INVALID_DECISIONInvalid platform or emulator assignment submitted
EMULATOR_REQUIRED / AMBIGUOUS_PLATFORM / MISSING_BIOSLaunch Doctor preflight failure conditions
JOB_STATE_CONFLICTDurable operation scheduling or state conflict
JOB_NOT_CANCELLABLE / JOB_NOT_RESUMABLEOperation transition disallowed in current state
LIBRARY_SYNC_UNAVAILABLELegacy full-library publish/pull is disabled; use the v3 preview/apply/publish catalog transport
SYNC_NOT_ENABLEDLibrary synchronization must be explicitly enabled before catalog operations
SYNC_INVALID / SYNC_PREVIEW_STALE / SYNC_PUBLISH_STALECatalog event validation failed or the local state changed since preview/publication began

Limits that apply across routes

LimitValue
Request body65,536 bytes
Socket timeout30 seconds
Playlist members100,000 per playlist
History returned by /api/historylimit clamped to 1..500, default 100
Webhook configs32
Watch folders50

The shared read endpoint

GET/api/libraryAuthX-OpenBox-Token: TOKEN

The full public library projection, cached until the library file, media epoch, or plugin epoch changes.

Returns games, playlists, filter_presets, ra_configured, settings, discovery, and media_epoch. Each game includes every editable field (with "" defaults) plus computed flags: id (numeric index), game_id (stable), favorite, hidden, last_played, play_count, playtime_seconds, path_exists, has_cover/has_background/…, has_saves, has_documents, has_achievements, tags, custom_fields, store_catalog, store_installed, owned, and more.

In safe mode the library hook is skipped; otherwise plugin library hooks can transform the games list before it is returned (cached for 30 seconds, invalidated on state changes).

Security notes

  • Treat server.token like a password: it grants full read/write access to the library, media, settings, and destructive operations.
  • The API can read any local file path referenced by library entries (media, documents, saves). Keep local paths and exported library data private.
  • Never log tokens or paste library exports into issues. The diagnostic log redacts credentials.

See How OpenBoxGL works for the server, state store, and lifecycle, and Configuration for the data directory.