Chapter 2 · API

Request and response samples

Representative /api endpoints across auth, catalog, watchlist, reviews, follow/circles, profile, trailers, and search. Shapes come from OpenAPI typedefs, Express handlers, and /web DTOs. Tokens and emails below are anonymized placeholders — not assessment-login credentials.

Base URL: {SERVER_URL}/api or {NEXT_PUBLIC_API_URL}/api · Auth header scheme jwt <accessToken> · Passport JWT payload is { id, userType, region } · Live contract: /api-docs.json.

24Endpoints sampled
8Domains
v1 + v2Auth generations
0Real secrets in samples
How to read the samples Access tokens in v1 responses are prefixed JWT <jwt> (see createAndSaveAuthTokens). Clients then send Authorization: jwt JWT <jwt>. v2 adds sessionId + deviceId and stores hashed refresh rows in UserSessions. Placeholder values use alex@example.com, <access-token>, and public title tt1375666 (Inception).

Auth

POST /login Public · v1 · backend/src/routes/auth.ts · returns AuthTokensData

Email/password. appVersion is gated unless it is WEB. Refresh is stored on Users.refreshToken.

Public · v1 · backend/src/routes/auth.ts · returns AuthTokensData

Request

{
  "email": "alex@example.com",
  "password": "example-password",
  "appVersion": "WEB"
}

Response 200

{
  "userId": 1842,
  "userType": "GU",
  "accessToken": "JWT <access-token>",
  "refreshToken": "<opaque-refresh-token>"
}
POST /v2/login Public · v2 · backend/src/routes/v2/auth.ts · returns AuthTokensSessionData

deviceId is required on the typedef. Session + hashed refresh land in UserSessions.

Public · v2 · backend/src/routes/v2/auth.ts · returns AuthTokensSessionData

Request

{
  "email": "alex@example.com",
  "password": "example-password",
  "appVersion": "WEB",
  "deviceId": "WEB"
}

Response 200

{
  "userId": 1842,
  "sessionId": "7c2e1a90-4b11-4c0e-9a22-0f3d8c1b2a10",
  "deviceId": "WEB",
  "userType": "GU",
  "accessToken": "JWT <access-token>",
  "refreshToken": "<opaque-refresh-token>"
}
POST /google-login Public · v1 · GoogleLoginData · same pattern for /apple-login and /facebook-login

Public · v1 · GoogleLoginData · same pattern for /apple-login and /facebook-login

Request

{
  "token": "<google-id-token>",
  "guestAppId": null,
  "appVersion": "WEB"
}

Response 200

{
  "userId": 1842,
  "userType": "GU",
  "accessToken": "JWT <access-token>",
  "refreshToken": "<opaque-refresh-token>"
}
POST /v2/auth-token-refresh Public · v2 · body includes session + device · returns new AuthTokensSessionData

Public · v2 · body includes session + device · returns new AuthTokensSessionData

Request

{
  "userId": 1842,
  "refreshToken": "<opaque-refresh-token>",
  "sessionId": "7c2e1a90-4b11-4c0e-9a22-0f3d8c1b2a10",
  "deviceId": "WEB",
  "appVersion": "WEB"
}

Response 200

{
  "userId": 1842,
  "sessionId": "7c2e1a90-4b11-4c0e-9a22-0f3d8c1b2a10",
  "deviceId": "WEB",
  "userType": "GU",
  "accessToken": "JWT <access-token>",
  "refreshToken": "<new-opaque-refresh-token>"
}
POST /users Public · register · express-validator: email, firstName 1–255, lastName 1–255, url re…

Public · register · express-validator: email, firstName 1–255, lastName 1–255, url required · returns UserCreationResponse

Request

{
  "email": "alex@example.com",
  "password": "example-password",
  "firstName": "Alex",
  "lastName": "Rivera",
  "url": "https://nokioapp.com/confirm-email",
  "appVersion": "WEB"
}

Response 200

{
  "userId": 1842,
  "userType": "GU",
  "accessToken": "JWT <access-token>",
  "refreshToken": "<opaque-refresh-token>",
  "isUserCreated": true
}

Profile

GET /users/profile JWT · UserProfile · own profile including network counts

JWT · UserProfile · own profile including network counts

Request

Authorization: jwt JWT <access-token>

Response 200

{
  "id": 1842,
  "firstName": "Alex",
  "lastName": "Rivera",
  "email": "alex@example.com",
  "avatarLink": "https://app.example.com/uploads/avatar.png",
  "userType": "GU",
  "region": "US",
  "verified": false,
  "isPushOn": true,
  "reviews": 3,
  "friends": 12,
  "followers": 4,
  "followings": 9,
  "circleCount": 2,
  "recommendationGiven": 5,
  "recommendationReceived": 1,
  "description": "What to watch with friends.",
  "platforms": [{ "id": 1, "name": "Netflix", "displayName": "Netflix" }],
  "userFavoriteMovie": [],
  "userCurrentFavoriteMovie": [],
  "publicProfileVanityUrl": null
}
PATCH /users/profile JWT · not guest · firstName/lastName 1–255, city ≤255, zipCode ≤50 · returns updated …

JWT · not guest · firstName/lastName 1–255, city ≤255, zipCode ≤50 · returns updated UserProfile

Request

{
  "firstName": "Alex",
  "lastName": "Rivera",
  "city": "Austin",
  "description": "Weekend watcher.",
  "region": "US",
  "isPushOn": true,
  "platforms": [1, 4, 12]
}

Response 200

{
  "id": 1842,
  "firstName": "Alex",
  "lastName": "Rivera",
  "city": "Austin",
  "region": "US",
  "userType": "GU",
  "description": "Weekend watcher."
}
GET /web/profile/{userId} Public + cache · path is user id or vanity · returns WebPublicProfileDto · 404 body i…

Public + cache · path is user id or vanity · returns WebPublicProfileDto · 404 body is plain Page not found

Request

GET /api/web/profile/alex-rivera

Response 200

{
  "id": 1842,
  "firstName": "Alex",
  "lastName": "Rivera",
  "avatarLink": "https://app.example.com/uploads/avatar.png",
  "city": "Austin",
  "address": null,
  "verified": false,
  "reviews": 3,
  "followers": 4,
  "followings": 9,
  "publicProfileVanityUrl": { "vanity": "alex-rivera" },
  "userFavoriteMovie": [],
  "userCurrentFavoriteMovie": []
}

Movies

GET /movies/v2/{slug} JWT · slug or tt…/nk… id · withReview query optional

JWT · slug or tt…/nk… id · withReview query optional

Request

GET /api/movies/v2/inception
Authorization: jwt JWT <access-token>

Response 200

{
  "imdbId": "tt1375666",
  "originalTitle": "Inception",
  "year": "2010",
  "type": "movie",
  "imdbRating": 8.8,
  "description": "A thief who steals corporate secrets through dream-sharing.",
  "poster": "https://app.example.com/uploads/inception.jpg",
  "movieSlug": "inception",
  "website": "https://www.imdb.com/title/tt1375666/?ref_=ref_ext_nokioapp",
  "isInWatchList": false,
  "isReviewed": false
}
GET /web/movie/v2/{slug} Web public + cache · Era B DTO · MovieDto extends ShortMovieDto

Web public + cache · Era B DTO · MovieDto extends ShortMovieDto

Request

GET /api/web/movie/v2/inception

Response 200

{
  "imdbId": "tt1375666",
  "originalTitle": "Inception",
  "year": "2010",
  "type": "movie",
  "imdbRating": 8.8,
  "language": "en",
  "movieSlug": "inception",
  "description": "A thief who steals corporate secrets through dream-sharing.",
  "poster": "https://image.tmdb.org/t/p/w500/example.jpg",
  "hasReviews": true,
  "reviewsCount": 128,
  "avgReviewsRate": 1600,
  "mamiMovie": false,
  "festivalMovie": false,
  "genres": [{ "id": 1, "name": "Action" }],
  "persons": [{ "id": 77, "name": "Leonardo DiCaprio", "role": "actor" }],
  "streamingPlatforms": [{ "id": 1, "name": "Netflix" }],
  "trailers": { "data": [], "totalItemsCount": 0, "hasMore": false, "currentPageNumber": 1 }
}
POST /movies/{id}/recommendations JWT · not guest · RecommendationsAddData · message required

JWT · not guest · RecommendationsAddData · message required

Request

POST /api/movies/tt1375666/recommendations
{
  "message": "Watch this before the sequel talk.",
  "contacts": [1901, 1902],
  "circles": [44],
  "toAllFollowers": false,
  "toAllContacts": false,
  "excludeContacts": [],
  "excludeCircles": [],
  "reviewId": null
}

Response 200

{
  "recommended": true
}

Watchlist

GET /watchlist JWT · filters: genres, type, platforms, languages, order, page, limit · returns Movie…

JWT · filters: genres, type, platforms, languages, order, page, limit · returns MoviesWatchlist

Request

GET /api/watchlist?page=1&limit=20&order=date
Authorization: jwt JWT <access-token>

Response 200

{
  "data": [
    {
      "imdbId": "tt1375666",
      "originalTitle": "Inception",
      "year": "2010",
      "type": "movie",
      "imdbRating": 8.8,
      "isInWatchList": true,
      "isReviewed": false,
      "movieSlug": "inception",
      "genres": ["Action", "Sci-Fi"],
      "languages": ["en"]
    }
  ],
  "totalItemsCount": 1,
  "currentPageNumber": 1
}
POST /watchlist JWT · fans out to all profile userIds · also marks MovieForUser.isDeleted

JWT · fans out to all profile userIds · also marks MovieForUser.isDeleted

Request

{
  "movieId": "tt1375666",
  "platformId": 1
}

Response 200

{
  "added": true
}
DELETE /watchlist/{movieId} JWT · 404 error.movie-not-found-in-watchlist if the row is missing

JWT · 404 error.movie-not-found-in-watchlist if the row is missing

Request

DELETE /api/watchlist/tt1375666
Authorization: jwt JWT <access-token>

Response 200

{
  "deleted": true
}

Reviews

POST /movies/{id}/review JWT · not guest · description string required · rate 0–2000 · path is imdbId or slug …

JWT · not guest · description string required · rate 0–2000 · path is imdbId or slug · returns Review

Request

POST /api/movies/tt1375666/review
{
  "description": "Still the gold standard for nested heist movies.",
  "rate": 1800,
  "platform": 1,
  "videoLink": null,
  "bothProfile": false,
  "onlyPublic": false
}

Response 200

{
  "id": 90114,
  "rate": 1800,
  "review": "Still the gold standard for nested heist movies.",
  "userId": 1842,
  "movieId": "tt1375666",
  "platformId": 1,
  "videoLink": null,
  "language": "en",
  "createdAt": "2026-09-04T09:12:00.000Z",
  "updatedAt": "2026-09-04T09:12:00.000Z"
}
GET /web/review/{reviewId} Public · ReviewDto with nested short movie + platform

Public · ReviewDto with nested short movie + platform

Request

GET /api/web/review/90114

Response 200

{
  "id": "90114",
  "movieId": "tt1375666",
  "rate": 1800,
  "review": "Still the gold standard for nested heist movies.",
  "videoLink": null,
  "movieSlug": "inception",
  "Movie": {
    "imdbId": "tt1375666",
    "originalTitle": "Inception",
    "year": "2010",
    "movieSlug": "inception"
  },
  "platform": { "id": 1, "name": "Netflix" }
}

Follow / circles

GET /users/contacts JWT · network list · query: limit, page, search, circleId, orderField

JWT · network list · query: limit, page, search, circleId, orderField

Request

GET /api/users/contacts?page=1&limit=20&search=
Authorization: jwt JWT <access-token>

Response 200

{
  "data": [
    {
      "id": 1901,
      "firstName": "Jordan",
      "lastName": "Lee",
      "avatar": "jordan.png",
      "avatarLink": "https://app.example.com/uploads/jordan.png",
      "verified": false,
      "isMemberOfCircle": false
    }
  ],
  "totalItemsCount": 1,
  "currentPageNumber": 1
}
POST /contacts JWT · follow / invite · url required · cannot follow self · returns true on success

JWT · follow / invite · url required · cannot follow self · returns true on success

Request

{
  "userId": 1901,
  "url": "https://nokioapp.com/invite"
}

Response 200

true
GET /users/contacts/{id}/following JWT · who that contact follows · sibling routes: /followers, /mutual-friends

JWT · who that contact follows · sibling routes: /followers, /mutual-friends

Request

GET /api/users/contacts/1901/following?page=1&limit=20
Authorization: jwt JWT <access-token>

Response 200

{
  "data": [
    {
      "id": 1842,
      "firstName": "Alex",
      "lastName": "Rivera",
      "avatarLink": "https://app.example.com/uploads/avatar.png",
      "verified": false
    }
  ],
  "totalItemsCount": 1,
  "currentPageNumber": 1
}
GET /circles JWT · own circles · query search, page, limit, movieOrder

JWT · own circles · query search, page, limit, movieOrder

Request

GET /api/circles?page=1&limit=20&search=
Authorization: jwt JWT <access-token>

Response 200

{
  "data": [
    {
      "id": 44,
      "name": "Weekend watch",
      "avatar": null,
      "avatarLink": null,
      "userId": 1842,
      "createdAt": "2026-04-12T18:00:00.000Z",
      "updatedAt": "2026-04-12T18:00:00.000Z"
    }
  ],
  "totalItemsCount": 1,
  "currentPageNumber": 1
}
POST /circles JWT · GU only · name required · optional jpeg/png avatar + contacts[] · returns Circl…

JWT · GU only · name required · optional jpeg/png avatar + contacts[] · returns Circle (sharedToken stripped in toJSON)

Request

{
  "name": "Weekend watch",
  "contacts": [1901, 1902]
}

Response 200

{
  "id": 44,
  "name": "Weekend watch",
  "avatar": null,
  "avatarLink": null,
  "userId": 1842,
  "createdAt": "2026-09-04T09:20:00.000Z",
  "updatedAt": "2026-09-04T09:20:00.000Z"
}
GET /web/circles Public + cache · despite the path, CirclesPageDataDto is an array of UserWithReviewsD…

Public + cache · despite the path, CirclesPageDataDto is an array of UserWithReviewsDto (public profiles), not Circles rows

Request

GET /api/web/circles?page=1&limit=20

Response 200

{
  "data": [
    {
      "id": 1901,
      "firstName": "Jordan",
      "lastName": "Lee",
      "avatarLink": "https://app.example.com/uploads/jordan.png",
      "verified": false,
      "publicProfileVanityUrl": { "vanity": "jordan-lee" },
      "userProperty": null,
      "moviesWithReviews": { "data": [], "totalItemsCount": 0, "hasMore": false, "currentPageNumber": 1 }
    }
  ],
  "totalItemsCount": 1,
  "hasMore": false,
  "currentPageNumber": 1
}

Trailers

GET /v2/homepage/trailers JWT · query genres[], type[], platforms[], page, limit · HomePageTrailerPageDataDto

JWT · query genres[], type[], platforms[], page, limit · HomePageTrailerPageDataDto

Request

GET /api/v2/homepage/trailers?page=1&limit=12
Authorization: jwt JWT <access-token>

Response 200

{
  "data": [
    {
      "movieId": "tt1375666",
      "trailerDate": "2026-08-01T00:00:00.000Z",
      "movieSlug": "inception",
      "Movie": {
        "imdbId": "tt1375666",
        "originalTitle": "Inception",
        "year": "2010",
        "movieSlug": "inception"
      },
      "platform": { "id": 1, "name": "Netflix" },
      "trailer": {
        "id": 501,
        "key": "YoHD9XEInc0",
        "site": "YouTube",
        "size": 1080,
        "type": "Trailer",
        "official": true
      }
    }
  ],
  "totalItemsCount": 1,
  "hasMore": false,
  "currentPageNumber": 1
}
GET /web/movie/{movieId}/trailers Public + cache · same DTO family as homepage trailers

Public + cache · same DTO family as homepage trailers

Request

GET /api/web/movie/tt1375666/trailers?page=1&limit=10

Response 200

{
  "data": [
    {
      "id": 501,
      "movieId": "tt1375666",
      "key": "YoHD9XEInc0",
      "site": "YouTube",
      "size": 1080,
      "type": "Trailer",
      "official": true,
      "publishedAt": "2010-05-10T00:00:00.000Z",
      "MovieSeason": null
    }
  ],
  "totalItemsCount": 1,
  "hasMore": false,
  "currentPageNumber": 1
}
GET /movies/{id}/allTrailers JWT · mobile / authenticated catalog · paginated trailer list

JWT · mobile / authenticated catalog · paginated trailer list

Request

GET /api/movies/tt1375666/allTrailers?page=1&limit=10
Authorization: jwt JWT <access-token>

Response 200

{
  "data": [
    {
      "id": 501,
      "movieId": "tt1375666",
      "key": "YoHD9XEInc0",
      "site": "YouTube",
      "type": "Trailer",
      "official": true
    }
  ],
  "totalItemsCount": 1,
  "currentPageNumber": 1
}
Not a full catalog Express mounts far more than these 24 routes — festival CRUD, admin Excel landings, chat sockets, stories, prompts, PPU convert, account delete. The OpenAPI file already documents them. Mobile consumes several that web does not. New work should land in /web or /v2 with DTOs, not in movies.ts / admin.ts.
Previous Interactive schema Next User journeys