// 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; the favicon is served at/favicon.svgand/favicon.ico. Every other 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, and unauthenticated requests to any route, known or unknown, return 403 {"error":"Unauthorized"}.
Request lifecycle
A request flows through a fixed set of stages. Knowing the order tells you which status code you will get and why:
parse + route
├─ unknown route? → 404 ROUTE_NOT_FOUND
└─ known route, wrong/missing token? → 403 UNAUTHORIZED
authenticate (secrets.compare_digest)
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
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)
Every error response carries 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.
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. 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.
Common errors
| Status | Envelope | When |
|---|---|---|
| 403 | {"error":"Unauthorized","code":"UNAUTHORIZED","request_id":"…"} | Missing or wrong token |
| 400 | {"error": "<message>", "code": "BAD_REQUEST", "request_id": "…"} | Validation failures, missing prerequisites surfaced by handlers, provider errors, unknown queue/notification actions, malformed payloads |
| 404 | {"code": "ROUTE_NOT_FOUND"} | Unknown route |
| 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
The stable contract lives at /api/v1/*. Legacy /api/* paths keep serving the same handlers for older clients. The v1 prefix currently covers the library, settings, launch, games, queue, tags, notifications, webhooks, playlists, sessions, saves, media, metadata, imports, emulators, profiles, themes, updates, backups, jobs, diagnostics, plugins, recovery, and filter presets. New work targets v1.
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 3 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.