// 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-Lengthmust 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 return400 {"error": ...}. - GET requests take parameters from the query string (
parse_qs); lists and repeated keys behave perparse_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
| Status | Envelope | When |
|---|---|---|
| 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) |
| 404 | GAME_NOT_FOUND / MEDIA_NOT_FOUND / DOCUMENT_NOT_FOUND / PLATFORM_DOCUMENT_NOT_FOUND / BADGE_NOT_FOUND | Lookups 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 viav1_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 asGET /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=1for 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, andPOST /api/v2/library/sync/publishwith{"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 andPOST /api/v2/library/sync/pullreturn503 LIBRARY_SYNC_UNAVAILABLEbefore mutation. - SQLite Search (v1.9.0):
GET /api/v2/library/search?q=&limit=(FTS5 or LIKE fallback;OPENBOX_ENABLE_SQLITE_READ=1opts 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, andPOST /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, andPOST /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, andPOST /api/v2/library/query/parse. Time Machine is journal-backed and bounded; revert previews must be reviewed and applied with a currentbase_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, andPOST /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, andPOST /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, andPOST /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, andPOST /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, andPOST /api/v2/steamgrid/match. The provider requiresSTEAMGRIDDB_API_KEY, is optional, and returns durable job IDs for apply and bulk match. - OpenBox launcher trophies (v1.11.0):
GET /api/v2/insights/trophiesandPOST /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, andPOST /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_envandlaunch_confirmfields on the game record, plus the opt-in weekly automatic backup settingsbackup_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), andPOST /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; theartworkdimension is the Artwork Doctor), andPOST /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, andPOST /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, andPOST /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), andPOST /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, andGET/POST /api/v2/plugins/settings.
- Setup Center:
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
| Code | Meaning |
|---|---|
PREVIEW_NOT_FOUND / PREVIEW_STALE / PREVIEW_EXPIRED / PREVIEW_LIBRARY_CHANGED | Setup preview session expired, stale, or invalidated by a library change |
PREVIEW_LIMIT_EXCEEDED / PREVIEW_ENTRY_LIMIT_EXCEEDED | Preview exceeds the entry or payload limit |
UNRESOLVED_CANDIDATES | Commit attempted while ambiguous candidates remain unresolved |
INVALID_DECISION | Invalid platform or emulator assignment submitted |
EMULATOR_REQUIRED / AMBIGUOUS_PLATFORM / MISSING_BIOS | Launch Doctor preflight failure conditions |
JOB_STATE_CONFLICT | Durable operation scheduling or state conflict |
JOB_NOT_CANCELLABLE / JOB_NOT_RESUMABLE | Operation transition disallowed in current state |
LIBRARY_SYNC_UNAVAILABLE | Legacy full-library publish/pull is disabled; use the v3 preview/apply/publish catalog transport |
SYNC_NOT_ENABLED | Library synchronization must be explicitly enabled before catalog operations |
SYNC_INVALID / SYNC_PREVIEW_STALE / SYNC_PUBLISH_STALE | Catalog event validation failed or the local state changed since preview/publication began |
Limits that apply across routes
| Limit | Value |
|---|---|
| Request body | 65,536 bytes |
| Socket timeout | 30 seconds |
| Playlist members | 100,000 per playlist |
History returned by /api/history | limit clamped to 1..500, default 100 |
| Webhook configs | 32 |
| Watch folders | 50 |
The shared read endpoint
/api/libraryAuthX-OpenBox-Token: TOKENThe 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.tokenlike 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.