API Reference
The iOS app runs on this JSON API, and so can anything else you write. Point the playground at your own server to send a real request to every endpoint below.
Registration, login and token refresh are public. Every other
/api/v1 endpoint needs an
Authorization: Bearer access token. Run
Login below and the playground copies the
access token into the field here. The one exception is the
Jellyfin webhook. It takes the
Jellyfin token from the setup guide
in the same header.
Requests go straight from your browser, so your server's CORS
policy (CLIENT_URL) has to allow this origin. Use
HTTPS for a remote server: this page is served over HTTPS, and
browsers block plain HTTP requests from it. Loopback addresses are
the exception, though the details vary by browser.
Health
/live Liveness probe. No authentication required.
{ "result": "alive" } /ready Readiness probe — 503 until Postgres answers. No authentication required.
{ "result": "ready" } /health Plain-text heartbeat, answered by middleware ahead of the router. Accepts HEAD as well as GET. No authentication required.
. Users
/api/v1/users/registrations Create a new account.
/api/v1/users/sessions Log in. After a successful login the playground copies the access token into the Token field above.
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6..."
} /api/v1/users/tokens Exchange a refresh token for a new token pair.
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6..."
} Account
/api/v1/accounts/me The authenticated user.
/api/v1/accounts Update profile and appearance preference.
/api/v1/accounts/stats Library counts, watch-time totals and watch activity for the selected period.
week, month and year mean the current calendar week, month and year, not a rolling window. Inside one of them, want counts what was added rather than the whole list, and the watching and playing counts are omitted. Defaults to all.
{
"period": "all",
"movies": { "want": 131, "watched": 895, "minutes": 104760 },
"series": { "want": 16, "watching": 75, "watched": 96 },
"episodes": { "watched": 4213, "minutes": 168520 },
"games": { "want": 12, "playing": 2, "played": 34, "minutes": 61200 },
"activity": [
{ "date": "2025-01-01", "movieMinutes": 12480, "tvMinutes": 41320 },
{ "date": "2026-01-01", "movieMinutes": 9260, "tvMinutes": 33970 }
]
} Integrations
/api/v1/accounts/integrations/jellyfin Create a Jellyfin token for the current user. A second call replaces the first token.
The token is shown once. The API stores only its SHA-256. It can add and remove watched marks in your library, so treat it as a password. The setup guide explains where it goes.
{
"token": "3f9c2b1e8d7a6c5b4e3f2a1d0c9b8a7f..."
} /api/v1/accounts/integrations/jellyfin Revoke the Jellyfin token. Events carrying it get 401 from then on. Also 204 when there was no token.
/api/v1/webhooks/jellyfin The endpoint the Jellyfin Webhook plugin posts to. It takes the Jellyfin token, not the access token. A Jellyfin token can add and remove watched marks in its owner's library, so this page never sends it. Try the endpoint from your own machine against your own server.
Played true with SaveReason PlaybackFinished or TogglePlayed adds a watched mark. Played false with SaveReason TogglePlayed takes one away. Every other save is dropped. A movie resolves through Provider_tmdb, an episode through Provider_tvdb or Provider_imdb. A title that is not in the library, or is already in the state the event asks for, is ignored with 204. A title biinge cannot resolve gets 422, a TMDB failure 502 and a database failure 503. Every accepted event is stored with its status.
curl -X POST <your API base>/api/v1/webhooks/jellyfin \
-H "Authorization: Bearer <Jellyfin token>" \
-H "Content-Type: application/json" \
-d '{
"ItemType": "Episode",
"SaveReason": "PlaybackFinished",
"Played": true,
"Provider_tvdb": "3254641",
"Provider_imdb": "tt1480055",
"Name": "Winter Is Coming"
}' Movies
/api/v1/movies Your movie library, one state at a time. type defaults to want.
{
"data": [
{
"id": 693134,
"title": "Dune: Part Two",
"posterPath": "/czembW0Rk1Ke7lCJGahbOhdCLZ.jpg",
"pinned": true,
"state": "want"
}
],
"meta": { "page": 1, "per": 24, "total": 1 }
} /api/v1/movies Add a movie to the library. id is the TMDB movie id.
/api/v1/movies/{id} Movie details, credits, and recommendations.
/api/v1/movies/{id} Change state or pin the movie.
/api/v1/movies/{id} Remove the movie from the library.
Games
/api/v1/games Your game library, one state at a time. type defaults to want.
/api/v1/games Add a game to the library. id is the IGDB game id, and posterPath is an IGDB cover image_id rather than a path.
/api/v1/games/{id} Game details from IGDB, with similar games and IGDB's estimated time for a main playthrough and for 100% completion, both in minutes.
/api/v1/games/{id} Change state or pin the game.
/api/v1/games/{id} Remove the game from the library.
TV Series
/api/v1/series Your series library, one state at a time. type defaults to want.
/api/v1/series Add a show to the library. id is the TMDB series id.
status is TMDB's own, and it matters: a show left at Returning Series never derives to watched, however many episodes are ticked.
/api/v1/series/{id} Series details with seasons.
/api/v1/series/{id} Change state or pin the show.
/api/v1/series/{id} Remove the show from the library.
/api/v1/series/{id}/season/{seasonNumber} Season details and episodes.
/api/v1/series/{id}/season/{seasonNumber}/episode/{episodeNumber} Single episode details.
Progress
/api/v1/series/{id}/progress Watched seasons and episodes for a show.
/api/v1/series/{id}/watched Mark a show watched. Ids come from the URL, and the body carries the rows to write.
Only the seasons and episodes you list are recorded, so a body without them marks nothing. The example lists two episodes of season one; a real client sends all nine, and every other season too.
/api/v1/series/{id}/watched Unmark the whole show, dropping every season and episode recorded for it.
/api/v1/series/{id}/seasons/{seasonId}/watched Mark a season watched.
The episodes array is what actually gets recorded. Send the season's episodes in it — without them the show ends up with no watched episodes, and nothing is tracked.
/api/v1/series/{id}/seasons/{seasonId}/watched Unmark a season and its episodes.
/api/v1/series/{id}/seasons/{seasonId}/episodes/{episodeId}/watched Mark an episode as watched.
The show and season rows are created from this body if they don't exist yet, so send their metadata alongside the episode.
/api/v1/series/{id}/seasons/{seasonId}/episodes/{episodeId}/watched Unmark an episode.
Search & Trending
/api/v1/search/movies Search movies. query is required.
/api/v1/search/series Search TV shows. query is required.
/api/v1/search/games Search PlayStation 5 and PlayStation 4 games. query is required.
/api/v1/search/people Search people. query is required.
/api/v1/trending/movies Trending movies.
/api/v1/trending/series Trending TV shows.
/api/v1/trending/games Trending PlayStation games.
/api/v1/trending/people Trending people.
Up Next
/api/v1/up-next The next aired episode you haven't watched of each pinned show you're watching, plus released movies and games from your pinned want lists.
{
"episodes": [
{
"seriesId": 125988,
"seriesTitle": "Silo",
"seriesPosterPath": "/gMYZZvnkVNTqSVnVCphWbPXwWwb.jpg",
"id": 7173957,
"title": "Who Are You?",
"seasonNumber": 3,
"number": 1,
"posterPath": "/nIvrv95n7gukebcCEUeZJDy9iEp.jpg",
"overview": "Juliette wakes somewhere she was never meant to reach…",
"runtime": 56,
"rating": 6.6,
"airDate": "2026-07-02"
}
],
"movies": [
{
"id": 786892,
"title": "Furiosa: A Mad Max Saga",
"posterPath": "/iADOJ8Zymht2JPMoy3R7xceZprc.jpg",
"overview": "As the world falls, young Furiosa is snatched from the Green Place…",
"runtime": 149,
"rating": 7.4,
"releaseDate": "2024-05-22"
}
],
"games": [
{
"id": 1942,
"title": "The Witcher 3: Wild Hunt",
"posterPath": "co1wyy",
"overview": "A story-driven open world RPG set in a fantasy universe…",
"runtime": 3000,
"rating": 9.5,
"releaseDate": "2015-05-19"
}
]
} People
/api/v1/people/{id} Person details and notable credits.
Reference
Common errors
A failure inside a controller returns a JSON body with an
error message and one of these status codes. Responses
from the middleware around it — rate limiting, an unmatched route —
are plain text.
| Status | Meaning | When |
|---|---|---|
400 | Bad Request | Malformed JSON, an unparseable id, or a body that fails validation |
401 | Unauthorized | Missing, invalid or expired access token, or wrong login credentials |
409 | Conflict | Registering a login or email that is already taken |
422 | Unprocessable Entity | The request was well formed but the work failed — including a title the upstream provider doesn't have |
429 | Too Many Requests | More than 5 requests a minute per IP to the public /users routes, 120 a minute per IP to the webhook, or 120 a minute per user everywhere else |
500 | Internal Server Error | Registration failed for a reason the API won't detail |
502 | Bad Gateway | TMDB failed while a Jellyfin event was being resolved |
503 | Service Unavailable | A dependency is unreachable, from /ready, from the auth middleware or from the Jellyfin webhook |
There is no 404: a title that isn't in your library, or
that TMDB and IGDB can't resolve, comes back as 422.