Wren TVDocs

Developers

Client API

The HTTP API players use to sign in, browse, play and report progress.

Every Wren TV server has a JSON API on the same port as its web app (32500). The web player, the apps and the server's own web app all use it. This page is a summary. The API may still change during early access.

Basics

  • Base URL: any of the server's addresses, such as http://192.168.1.20:32500 or its wrentv.net address.
  • Bodies are JSON, both ways.
  • Errors come back as {"error": "message"} with a 4xx or 5xx status.
  • CORS is open (Access-Control-Allow-Origin: *) on every route, including streams, so browser apps on any origin can use it.

Server info

GET /api/server needs no sign-in. It tells a client what kind of server it's talking to:

json{
  "name": "Living Room",
  "machineId": "3f9c…",
  "version": "0.1.0",
  "authMode": "supabase",
  "setupRequired": false,
  "streaming": true,
  "localUrls": ["https://192-168-1-20.ab12cd.wrentv.net:32500"],
  "supabase": { "url": "https://….supabase.co", "publishableKey": "sb_publishable_…" },
  "serverId": "…"
}

GET /api/health returns {"status": "ok"}.

Authentication

Send a bearer token on every other request:

httpAuthorization: Bearer <token>
  • Wren TV accounts (authMode: "supabase"): the token is the user's Supabase access token, from signing in to Wren TV accounts with email and password. The server checks it, and asks Wren TV accounts whether the user may use this server.

  • Local accounts (authMode: "local"): sign in to the server itself.

    bashcurl -X POST http://localhost:32500/api/auth/login \
         -d '{"username":"me","password":"…","device":"My App"}'
    # → {"token": "…", "user": {…}}

GET /api/me returns the signed-in user, their role on this server, the library IDs they can see, and their limits: {"canStream", "canDownload", "maxStreams", "maxDownloads"} (0 means no limit).

Browsing

MethodPathReturns
GET/api/homeContinue watching, recently added per library, Home collections and more
GET/api/librariesLibraries the user can see
GET/api/libraries/{id}/itemsA library's movies or shows. Query: sort (title, added, year, rating), genre, q, unwatched=1, limit (default 500), offset
GET/api/items/{id}One item, with its files and the user's progress
GET/api/items/{id}/childrenA show's seasons, or a season's episodes
GET/api/items/{id}/extrasCast and related titles
GET/api/search?q=Movies, shows and episodes matching a title (up to 60)
GET/api/genres, /api/genres/items?genre=Genres, and the titles in one
GET/api/collections, /api/collections/{id}Collections, and one collection's titles
GET/api/discover/trendingTrending titles on TMDB
GET/api/images/{size}/{file}Cached artwork. Needs no token

Lists only ever include libraries the user has access to.

Playback

Start playing

POST /api/items/{id}/play with what the client can decode:

json{
  "caps": {
    "engines": ["avplayer"],
    "nativeVideo": ["h264", "hevc"],
    "nativeAudio": ["aac", "ac3", "eac3", "flac"],
    "nativeContainers": ["mp4", "mov"],
    "dolbyVision": false,
    "hdr": true,
    "maxKbps": 0,
    "audioFallback": "flac"
  },
  "fileId": null,
  "audioIndex": null,
  "maxWidth": 0
}
FieldMeans
enginesThe client's players, in preference order: avplayer (a native, AVPlayer-class engine) and/or mpv (libmpv)
nativeVideo, nativeAudio, nativeContainersWhat the native engine decodes
dolbyVision, hdrWhether the screen shows them
maxKbpsBitrate limit; 0 for none
audioFallbackWhat unsupported audio becomes during a remux: alac, flac or aac
forceEngine, forceTranscodeFor testing
fileIdA specific version; omit to let the server choose
audioIndexAn audio track; omit to use the user's language preference
maxWidthScale transcodes down to this width

The response says how to play it:

json{
  "status": "ready",
  "sessionId": "…",
  "engine": "avplayer",
  "method": "remux",
  "url": "/stream/<session>/main.m3u8",
  "decision": { "videoCopy": true, "audioCopy": true, "audioCodec": "eac3", "reasons": ["remux mkv to fMP4 (no re-encode)"] },
  "duration": 7342.1,
  "resumePosition": 1810,
  "subtitles": [{ "index": 0, "language": "en", "codec": "subrip", "forced": false, "url": "…" }],
  "subtitleIndex": -1,
  "chapters": [],
  "markers": { "intro": { "start": 62.5, "end": 151.0 }, "source": "fingerprint" },
  "versions": [{ "id": "…", "label": "4K · Dolby Vision · 58.2 GB", "height": 2160 }]
}
  • method is direct (the raw file, with range requests), remux or transcode (both HLS with fMP4 segments).
  • url is relative to the server: /stream/{session}/main.m3u8 for HLS, or /stream/{session}/file.<ext> for direct play. Stream URLs need no token: the session ID is the credential, because players can't always send headers. They stop working when the session ends.
  • markers has intro and credits ranges when known, and source says where they came from (chapters or fingerprint).
  • subtitles[].url gives WebVTT for text subtitles. While a track is still being converted, the response has X-Subtitle-Complete: false; fetch it again until it's true.
  • 202 {"status": "pending"} means a streamed title is still finding a stream. Show the message and retry every few seconds.
  • 424 means no cached release was found for a streamed title, and 503 means the server is at its stream limit. Both have an error message to show.
  • 429 means the user is at their own limit of streams at once.

Report progress and stop

MethodPathBody
POST/api/items/{id}/progress{"position": 1830.5, "duration": 7342.1} in seconds. Send it every few seconds while playing
POST/api/items/{id}/watched{"watched": true}. On a show or season, marks every episode
DELETE/api/sessions/{sessionId}Ends the playback session, stopping any remux or transcode

Preferences

GET /api/me/prefs and PATCH /api/me/prefs read and change the user's playback languages: {"audioLanguage": "en", "subtitleLanguage": ""}, as ISO 639-1 codes. An empty subtitleLanguage means off.

Streaming

When streaming is true in /api/server and the user's limits.canStream:

MethodPathBody or returns
GET/api/discover/search?q={"streaming": true, "items": [ {"tmdbId", "mediaType", "title", "year", "posterPath", "item"} ]}. item is set when the server already has the title
POST/api/streaming/titles{"tmdbId": 693134, "mediaType": "movie"} (movie or show). Adds it to the streaming library and returns the item

The returned item plays like any other; the first play may answer 202 pending while a stream is found.

Downloads

POST /api/items/{id}/download with {"fileId": "…"} (optional; the best version otherwise) returns a link to the original file:

json{ "status": "ready", "url": "/download/<ticket>/Dune - Part Two (2024).mkv", "fileName": "…", "fileId": "…",
  "size": 61234567890, "label": "4K · Dolby Vision", "expiresAt": 1760086400 }

The URL needs no token (the ticket is the credential), supports range requests for resuming, and works for 24 hours. It answers 429 while the user has maxDownloads downloads running. For a streamed title with no stream yet, the response is 202 {"status": "pending"}: retry. canDownload on GET /api/items/{id} says whether to show the button.

Live updates

GET /api/events is a server-sent events stream. It sends library.updated, item.added, item.updated, scan.started and scan.finished events, so clients can refresh without polling.

Admin

Owners and admins also have endpoints to manage libraries (POST/PATCH/DELETE /api/libraries, POST /api/libraries/{id}/scan, POST /api/items/{id}/match), users and their limits (/api/users), collections, settings (/api/admin/settings), status (/api/admin/status), stats (/api/admin/stats, /api/admin/history), and streaming: server downloads (/api/admin/downloads, POST /api/items/{id}/server-download), saved streams (GET and DELETE /api/items/{id}/streams) and key checks (/api/admin/streaming/check-debrid, /api/admin/streaming/check-addon). These are what the Manage pages use.

For services that manage libraries without a user, use the integration API instead.

Wren TV is in early access. Something here wrong or missing? Tell us in the beta.