Repository navigation
REST API
Streamline's API is the same one its own web UI uses — there's no privileged internal surface. Anything the SPA can do, you can do.
- Interactive docs
- Authentication
- Conventions
- Endpoint map
- Media probe
- Worked examples
- Generating a client
| URL | What |
|---|---|
/api/docs |
Scalar UI — browse and try endpoints |
/api/v1/openapi.yaml |
The raw OpenAPI 3.0.4 spec |
The spec is the source of truth: the Go server types are generated from it with oapi-codegen, so it can't drift from the implementation.
Base URL for everything below: /api/v1.
Two credentials work on /api/v1/*:
# API key — for scripts and long-lived integrations
curl -H "X-API-Key: $KEY" https://streamline.example.com/api/v1/movies
# Bearer JWT — for a session obtained by logging in
curl -H "Authorization: Bearer $JWT" https://streamline.example.com/api/v1/moviesCookies are ignored on /api/v1 except for same-origin browser requests carrying Sec-Fetch-Site: same-origin — that's how the SPA authenticates without holding a second credential. Anything outside a browser needs a key or a token.
Failures return 401 with a JSON body. No redirects on the API surface.
The two credentials are equal on media and settings endpoints, but API keys are read-only on the identity surface: any non-GET request under /auth/me, /auth/password, /auth/invites, /auth/jwt, or /users returns 403 with a key — those actions need a session (Bearer JWT or the SPA cookie). That's why the key-creation example below authenticates with a JWT.
Account settings → API keys, or:
curl -X POST -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
-d '{"name":"my-script"}' \
https://streamline.example.com/api/v1/auth/me/api-keysThe raw key is returned once. A key inherits its owner's permissions — an admin's key is an admin key, so create read-only integrations under a member account.
curl -c cookies.txt -X POST -H 'Content-Type: application/json' \
-d '{"email":"you@example.com","password":"..."}' \
https://streamline.example.com/auth/loginNote the path: /auth/login, not /api/v1/auth/login. It returns 204 and sets streamline_session; the cookie's value is the JWT, so you can lift it out and use it as a Bearer token.
For anything non-interactive, use an API key instead.
Pagination. Collection endpoints take ?page= (default 1) and ?limit= (default 20) and return an envelope:
{ "items": [ ... ], "total": 137, "page": 1, "limit": 20 }Activity feeds use cursor pagination instead (?cursor=, ?limit=), since they're append-only and time-ordered.
Name-keyed resources. Config-backed resources are addressed by name, not numeric ID:
/indexers/{name} /download-clients/{name}
/media-servers/{name} /quality-profiles/{name}
/schedules/{name}
Everything database-backed (movies, series, requests, users, imports) uses numeric IDs.
Update verbs are not uniform. Media servers use PATCH; indexers, download clients and quality profiles use PUT. This is a genuine inconsistency in the API, not a documentation error — check the spec if in doubt.
Secrets are never returned. Read views expose booleans — api_key_set, password_set, client_secret_set — instead of values. On update, sending a blank secret preserves the existing one rather than clearing it, so you can round-trip a config object without leaking or destroying credentials.
Errors are {"message": "..."} with a conventional status: 400 bad request, 401 unauthenticated, 403 forbidden (usually not an admin), 404, 409 conflict (already exists), 422 unprocessable, 500.
107 paths. Grouped, with admin-only marked 🔒.
| Method | Path |
|---|---|
GET POST
|
/movies |
GET |
/movies/counts |
GET PATCH DELETE
|
/movies/{id} |
POST |
/movies/{id}/search · /search-now · /grab · /refresh-metadata · /rename · /play-on
|
GET |
/movies/{id}/recommendations |
DELETE |
/movies/{id}/files/{fileId} |
GET |
/search/movie · /search/movie/{tmdb_id} — TMDB lookup |
| Method | Path |
|---|---|
GET POST
|
/series |
GET |
/series/counts · /series/lookup · /series/lookup/{tvdb_id}
|
POST |
/series/specials/apply |
GET PATCH DELETE
|
/series/{id} |
GET |
/series/{id}/browse |
POST |
/series/{id}/search · /grab · /refresh-metadata · /rename · /play-on
|
PATCH |
/series/{id}/seasons/{number} |
POST |
/series/{id}/seasons/{number}/search · /grab
|
GET PATCH
|
/series/{id}/episodes/{episodeId} |
POST |
/series/{id}/episodes/{episodeId}/search · /grab
|
DELETE |
/series/{id}/episodes/{episodeId}/file |
| Method | Path |
|---|---|
GET |
/activity — event feed |
GET |
/activity/queue · /activity/history
|
DELETE |
/activity/queue/{id} · /activity/history/{id}
|
POST |
/activity/queue/{id}/pause · /resume · /activity/history/clear-completed
|
GET |
/activity/pending 🔒 |
POST |
/activity/pending/{id}/import · /replace · /ignore 🔒 |
| Method | Path | Who |
|---|---|---|
GET POST
|
/requests |
Any (scoped for request_only) |
GET |
/requests/counts · /requests/{id}/metadata
|
Any |
POST |
/requests/{id}/approve |
admin, member |
POST |
/requests/{id}/deny · /reopen
|
admin |
| Method | Path |
|---|---|
GET POST
|
/indexers · /download-clients · /media-servers · /quality-profiles
|
GET DELETE
|
/{resource}/{name} |
PUT |
/indexers/{name} · /download-clients/{name} · /quality-profiles/{name}
|
PATCH |
/media-servers/{name} |
POST |
/{resource}/test — test an unsaved config |
POST |
/{resource}/{name}/test — test a saved one |
GET |
/media-servers/discover — list libraries/sections |
| Method | Path |
|---|---|
GET |
/torrents · /torrents/{hash}
|
POST |
/torrents/{hash}/pause · /resume
|
PATCH |
/torrents/{hash}/files/{index} — toggle a file |
| Method | Path |
|---|---|
GET POST
|
/library/imports |
GET DELETE
|
/library/imports/{id} |
POST |
/library/imports/{id}/cancel · /commit
|
GET |
/library/imports/{id}/files · /shows
|
PATCH |
/library/imports/{id}/files/{fileId} · /shows/{showId}
|
GET POST
|
/library/path-migration |
GET |
/library/path-migration/roots |
POST |
/library/path-migration/preview |
| Method | Path | Who |
|---|---|---|
GET PATCH
|
/auth/me |
Any |
PUT |
/auth/password |
Any |
GET POST
|
/auth/me/api-keys · /auth/me/sessions
|
Any |
DELETE |
/auth/me/api-keys/{id} · /auth/me/sessions/{id}
|
Any |
POST |
/auth/jwt/rotate |
🔒 |
GET POST
|
/auth/invites |
🔒 |
DELETE |
/auth/invites/{id} |
🔒 |
GET POST
|
/users |
🔒 |
GET PATCH DELETE
|
/users/{uid} |
🔒 |
POST |
/users/{uid}/password-reset · /unlock
|
🔒 |
DELETE |
/users/{uid}/api-keys/{kid} · /sessions/{sid}
|
🔒 |
| Method | Path |
|---|---|
GET PATCH
|
/config/auth · /config/library · /config/ffmpeg
|
GET POST
|
/config/oidc |
GET PATCH DELETE
|
/config/oidc/{name} |
GET |
/schedules · /schedules/{name}
|
PATCH |
/schedules/{name} |
POST |
/schedules/{name}/pause · /resume · /run
|
GET |
/system/info |
| Method | Path |
|---|---|
GET |
/calendar/upcoming?from=&to= — movie digital releases and episode air dates |
| Path | Notes |
|---|---|
GET /health |
Unauthenticated probe. Bare JSON, deliberately not in the spec |
POST /auth/login · /auth/register · /auth/logout
|
Cookie-based, 204 on success |
GET /auth/config · /auth/invite/{token}
|
Pre-auth SPA bootstrap |
GET /auth/oidc/{name}/start · /callback
|
The OIDC flow |
GET /posters/{kind}/{id}/poster.jpg |
Poster proxy |
Technical details read from your files with ffprobe — resolution, codecs, duration, bitrate. See Configuration Reference for the config side.
media_info is a nullable object on MediaFile (movies) and Episode responses:
{
"container": "matroska",
"video_codec": "hevc",
"width": 3840,
"height": 1608,
"duration_seconds": 8130,
"audio_codec": "eac3",
"audio_channels": 6,
"bitrate": 24500000,
"probed_at": "2026-08-18T12:00:00Z"
}It's absent until the file has been probed, and absent again if the probe failed — check for the key, don't assume it's always there. There's no per-stream breakdown (no audio_tracks/subtitles) in this release.
ffmpeg_warn on GET /system/info is true when ffmpeg.enabled is true but ffprobe wasn't found on this process — a misconfigured ffmpeg.path or a custom build missing the binaries. The key is absent when ffmpeg.enabled is false; the operator opted out, so it's not a warning.
GET/PATCH /config/ffmpeg (admin) reads and edits the runtime config:
api "$SL/api/v1/config/ffmpeg"
# {"enabled":true,"path":"","found":true,"resolved_path":"/usr/local/bin/ffprobe"}
api -X PATCH -d '{"enabled":false}' "$SL/api/v1/config/ffmpeg"found and resolved_path are derived from the current process's live prober, not the config file — read-only, sending them in the PATCH body has no effect. path only takes effect on the next restart, since the prober is built once at boot.
export SL=https://streamline.example.com
export KEY=your-api-key
api() { curl -sS -H "X-API-Key: $KEY" -H 'Content-Type: application/json' "$@"; }Add a movie by TMDB ID:
api -X POST -d '{"tmdb_id":603,"quality_profile":"default"}' "$SL/api/v1/movies"Find everything still wanted:
api "$SL/api/v1/movies?limit=100" | jq '.items[] | select(.status=="wanted") | .title'Trigger a search for every wanted movie:
api -X POST "$SL/api/v1/schedules/movie-missing-search/run"Add a show, monitoring only missing episodes:
api -X POST -d '{"tvdb_id":81189,"preset":"missing"}' "$SL/api/v1/series"preset is one of all, future, missing, existing, pilot, none, and is applied once at add time to the season/episode tree.
Approve every pending request:
api "$SL/api/v1/requests?status=pending" \
| jq -r '.items[].id' \
| xargs -I{} curl -sS -X POST -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-d '{}' "$SL/api/v1/requests/{}/approve"Nagios/Prometheus-style health check:
curl -fsS "$SL/health" >/dev/null && echo OKWatch the download queue:
watch -n5 "curl -sS -H 'X-API-Key: $KEY' $SL/api/v1/activity/queue \
| jq -r '.items[] | \"\(.status)\t\(.progress)\t\(.title)\"'"Add an indexer:
api -X POST -d '{
"name":"prowlarr",
"protocol":"prowlarr",
"host":"prowlarr",
"port":9696,
"api_key":"...",
"enabled":true
}' "$SL/api/v1/indexers"
api -X POST "$SL/api/v1/indexers/prowlarr/test"The spec is standard OpenAPI 3.0.4, so any generator works:
curl -fsSL -o openapi.yaml https://streamline.example.com/api/v1/openapi.yaml
# TypeScript
npx openapi-typescript openapi.yaml -o streamline.d.ts
# Python / Kotlin / Swift / …
npx @openapitools/openapi-generator-cli generate \
-i openapi.yaml -g python -o ./clientBuilding a mobile client is an explicitly supported use case — the API was designed for it, which is why every UI capability has an endpoint behind it.
🎬 Operating Streamline
- Installation
- First-Run Setup
- Adding Movies and TV
- Importing an Existing Library
- Activity and Calendar
- Requests and Users
- NixOS and Nix
- Troubleshooting
- Roadmap
⚙️ Advanced
- Configuration Reference
- Authentication and SSO
- Quality Profiles and Naming
- Quality Profiles and Custom Formats
- Scheduled Jobs
- REST API
- Observability and Logging
- GitOps and Kubernetes