REST API

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 token — run Login below and the playground copies the access token into the field here.

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

GET /live

Liveness probe. No authentication required.

Example response 200
{ "result": "alive" }
GET /ready

Readiness probe — 503 until Postgres answers. No authentication required.

Example response 200
{ "result": "ready" }
GET /health

Plain-text heartbeat, answered by middleware ahead of the router. Accepts HEAD as well as GET. No authentication required.

Example response 200
.

Users

POST /api/v1/users/registrations

Create a new account.

POST /api/v1/users/sessions

Log in. After a successful login the playground copies the access token into the Token field above.

Example response 200
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6..."
}
POST /api/v1/users/tokens

Exchange a refresh token for a new token pair.

Example response 200
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6..."
}

Account

GET /api/v1/accounts/me

The authenticated user.

PATCH /api/v1/accounts

Update profile and appearance preference.

GET /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.

Example response 200
{
  "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 }
  ]
}

Movies

GET /api/v1/movies

Your movie library, one state at a time. type defaults to want.

Example response 200
{
  "data": [
    {
      "id": 693134,
      "title": "Dune: Part Two",
      "posterPath": "/czembW0Rk1Ke7lCJGahbOhdCLZ.jpg",
      "pinned": true,
      "state": "want"
    }
  ],
  "meta": { "page": 1, "per": 24, "total": 1 }
}
POST /api/v1/movies

Add a movie to the library. id is the TMDB movie id.

GET /api/v1/movies/{id}

Movie details, credits, and recommendations.

PATCH /api/v1/movies/{id}

Change state or pin the movie.

DELETE /api/v1/movies/{id}

Remove the movie from the library.

Games

GET /api/v1/games

Your game library, one state at a time. type defaults to want.

POST /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.

GET /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.

PATCH /api/v1/games/{id}

Change state or pin the game.

DELETE /api/v1/games/{id}

Remove the game from the library.

TV Series

GET /api/v1/series

Your series library, one state at a time. type defaults to want.

POST /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.

GET /api/v1/series/{id}

Series details with seasons.

PATCH /api/v1/series/{id}

Change state or pin the show.

DELETE /api/v1/series/{id}

Remove the show from the library.

GET /api/v1/series/{id}/season/{seasonNumber}

Season details and episodes.

GET /api/v1/series/{id}/season/{seasonNumber}/episode/{episodeNumber}

Single episode details.

Progress

GET /api/v1/series/{id}/progress

Watched seasons and episodes for a show.

POST /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.

DELETE /api/v1/series/{id}/watched

Unmark the whole show, dropping every season and episode recorded for it.

POST /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.

DELETE /api/v1/series/{id}/seasons/{seasonId}/watched

Unmark a season and its episodes.

POST /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.

DELETE /api/v1/series/{id}/seasons/{seasonId}/episodes/{episodeId}/watched

Unmark an episode.

Search & Trending

GET /api/v1/search/movies

Search movies. query is required.

GET /api/v1/search/series

Search TV shows. query is required.

GET /api/v1/search/games

Search PlayStation 5 and PlayStation 4 games. query is required.

GET /api/v1/search/people

Search people. query is required.

Up Next

GET /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.

Example response 200
{
  "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

GET /api/v1/people/{id}

Person details and notable credits.

Reference

ERR 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.

StatusMeaningWhen
400Bad RequestMalformed JSON, an unparseable id, or a body that fails validation
401UnauthorizedMissing, invalid or expired access token, or wrong login credentials
409ConflictRegistering a login or email that is already taken
422Unprocessable EntityThe request was well formed but the work failed — including a title the upstream provider doesn't have
429Too Many RequestsMore than 5 requests a minute per IP to the public /users routes, or 120 a minute per user everywhere else
500Internal Server ErrorRegistration failed for a reason the API won't detail
503Service UnavailableA dependency is unreachable, from /ready or from the auth middleware

There is no 404: a title that isn't in your library, or that TMDB and IGDB can't resolve, comes back as 422.