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.
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.
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.
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
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
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…
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
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 …
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…
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
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
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
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
}
Search
GET
/movies
JWT · search required · optional page, limit, order · may hit OpenSearch (AB test B) …
Request
GET /api/movies?search=inception&page=1&limit=20&order=popularity Authorization: jwt JWT <access-token>
Response 200
{
"data": [
{
"imdbId": "tt1375666",
"originalTitle": "Inception",
"year": "2010",
"type": "movie",
"poster": "https://app.example.com/uploads/inception.jpg",
"movieSlug": "inception"
}
],
"totalItemsCount": 1,
"hasMore": false,
"currentPageNumber": 1
}
GET
/web/movie
Public + cache · search min length 1 · order in release | popularity | rating · Movie…
Request
GET /api/web/movie?search=inception&page=1&limit=20&order=popularity
Response 200
{
"data": [
{
"imdbId": "tt1375666",
"originalTitle": "Inception",
"year": "2010",
"poster": "https://app.example.com/uploads/inception.jpg",
"posterMd": "https://app.example.com/uploads/inception-md.jpg",
"movieSlug": "inception",
"Platform": { "id": 1, "name": "Netflix" }
}
],
"totalItemsCount": 1,
"hasMore": false,
"currentPageNumber": 1
}
Watchlist
GET
/watchlist
JWT · filters: genres, type, platforms, languages, order, page, limit · returns Movie…
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
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
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 …
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
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
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
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
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
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…
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…
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
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
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
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.