# kinodata API > kinodata links the IDs of movies and series across IMDb, Kinopoisk, TMDB and Kinorium, and uses those links to serve title data: titles in Russian and English, every source’s ratings, cast, media, relations, filmographies, collections and a release calendar. - The base URL is `https://api.kinodata.space/v1.3`. Every request is a `GET`; the key goes in the `X-API-Key` header. - A successful response is the data itself, with no wrapper. An error is `{"error": {"code", "message", "field", "allowed", "hint"}}` with a 4xx or 5xx status. Branch on `error.code`. - Text fields come in `_ru` and `_en` pairs. `lang=ru` or `lang=en` keeps one half. A missing translation is `null`, never a substitute. - An unknown parameter or value returns `400` with the allowed values in `allowed` and a suggestion in `hint`. Don’t invent parameters: take them from the reference below. - Every docs page is available as Markdown: add `.md` to its URL. --- # Introduction > What the kinodata API does, the base URL and the response format. > https://kinodata.space/en/docs.md kinodata links movie and series IDs across IMDb, Kinopoisk, TMDB and Kinorium. On top of those links the API serves title data: Russian and English titles, ratings from every source, cast and crew, stills and trailers, related titles, filmographies and collections. ## One movie, five IDs *Inception* has a different ID in every catalog, and nothing in the numbers says they are the same movie: | catalog | type in the path | ID | |---|---|---| | IMDb | `imdb` | `tt1375666` | | Kinopoisk | `kp` | `447301` | | TMDB, movies | `tmdb` | `27205` | | TMDB, series | `tmdb_tv` | — | | Kinorium | `kinorium` | `472809` | Any of them opens the same title: `/titles/imdb/tt1375666`, `/titles/kp/447301` and `/titles/tmdb/27205` return the same card. Use whichever ID your database already has. ## Base URL ```text https://api.kinodata.space/v1.3 ``` Paths in the reference are relative to it: `/ids/kp/447301` means `https://api.kinodata.space/v1.3/ids/kp/447301`. Every request is a `GET` over HTTPS, with your API key in the `X-API-Key` header. ## Response format JSON in UTF-8. A successful response is the data itself, with no wrapper. An error is an `error` object with a 4xx or 5xx status: ```json { "error": { "code": "not_found", "message": "title not found: kp 999999999" } } ``` Tell success from failure by the HTTP status, not by the shape of the body. The details are on the [Errors](https://kinodata.space/en/docs/errors.md) page. ## Languages Text fields come in pairs: `title_ru` and `title_en`, `name_ru` and `name_en`. The language is always in the field name. Add `lang=ru` or `lang=en` to keep one language in the response; the field names stay the same. When there is no translation the field is `null`, and falling back to another language or the original title is up to you. ## Keep the key on the server Your API key acts on your behalf. Keep it on a server or in an environment variable. In website or mobile app code, anyone who opens the developer tools can see it. ## Next - [Quickstart](https://kinodata.space/en/docs/quickstart.md): your first request in five minutes. - [Response languages](https://kinodata.space/en/docs/languages.md): how the `_ru` and `_en` fields work. - [IDs in every catalog](https://kinodata.space/en/docs/ids.md): the simplest endpoint. --- # Quickstart > An API key, your first request, a title card and search in five minutes. > https://kinodata.space/en/docs/quickstart.md In five minutes you will get an API key, link one movie's IDs across every catalog, open its title card and find a title by name. You need a terminal with `curl`, or any of the languages in the examples. ## Get an API key The key is free and comes with 500 requests a day. [How to get a key](https://kinodata.space/en/pricing). Save the key to an environment variable. Every example in these docs reads it from there, so it never ends up in your code. macOS and Linux: ```bash export KINODATA_KEY="your_key" ``` Windows, PowerShell: ```powershell $env:KINODATA_KEY = "your_key" ``` ## Link a movie's IDs Send the Kinopoisk ID of *Inception* and get its IDs in every catalog back: ```bash curl -H "X-API-Key: $KINODATA_KEY" \ "https://api.kinodata.space/v1.3/ids/kp/447301" ``` Response `200`: ```json { "imdb_id": "tt1375666", "kp_id": "447301", "tmdb_id": "27205", "tmdb_tv_id": null, "kinorium_id": "472809" } ``` Where: - `kp` is the catalog the ID comes from. Any of the five works: `imdb`, `kp`, `tmdb`, `tmdb_tv`, `kinorium`. - `447301` is the ID in that catalog. - `null` means the title has no ID in that catalog. *Inception* is a movie, so `tmdb_tv_id` is empty. ## Open the title card The same ID opens the title card. The `fields` parameter keeps only the blocks you ask for, here the titles and the kind: ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "fields=title,type" \ --data-urlencode "lang=en" \ "https://api.kinodata.space/v1.3/titles/kp/447301" ``` Response `200`: ```json { "ids": { "imdb_id": "tt1375666", "kp_id": "447301", "tmdb_id": "27205", "tmdb_tv_id": null, "kinorium_id": "472809", "urls": { "imdb": "https://www.imdb.com/title/tt1375666/", "kinorium": "https://en.kinorium.com/472809/", "kp": "https://www.kinopoisk.ru/film/447301/", "tmdb": "https://www.themoviedb.org/movie/27205" } }, "title": { "title_en": "Inception", "title_original": "Inception" }, "type": { "kind": "movie", "is_series": false, "status": "released", "kind_label_en": "Movie", "status_label_en": "Released" } } ``` The `ids` block always comes back. Without `fields` you get the whole card: up to 22 blocks, about 26 KB. Without `lang`, text fields come in `_ru` and `_en` pairs. `lang=en` keeps only the English half. ## Find a title by name No ID at hand? Search by title in either language. Search forgives typos: “incepton”, with a letter missing, finds *Inception*: ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "q=incepton" \ --data-urlencode "limit=1" \ --data-urlencode "lang=en" \ "https://api.kinodata.space/v1.3/titles" ``` Response `200`: ```json { "has_more": true, "items": [ { "imdb_id": "tt1375666", "kp_id": "447301", "tmdb_id": "27205", "tmdb_tv_id": null, "kinorium_id": "472809", "title_en": "Inception", "title_original": "Inception", "year": 2010, "release_date": "2010-07-15", "kind": "movie", "poster_url_en": "https://image.tmdb.org/t/p/original/xlaY2zyzMfkhk0HSC5VUwzoZPU1.jpg", "posters_by_source": { "kp": "https://avatars.mds.yandex.net/get-kinopoisk-image/1629390/8ab9a119-dd74-44f0-baec-0629797483d7/600x900", "tmdb": "https://image.tmdb.org/t/p/original/piQXcdOGgv1O9HQ07pI0tnjkGJw.jpg", "imdb": null, "kinorium": "https://images.kinorium.com/movie/poster/472809/w1500_37543056.jpg" }, "rating_kp": 8.7, "rating_tmdb": 8.4, "rating_imdb": 8.8, "rating_kr": 8.6, "rating_letterboxd": 8.4, "votes_kp": 1132562, "votes_tmdb": 40402, "votes_imdb": 2877173, "countries": [ { "code": "us", "name_en": "United States" }, { "code": "gb", "name_en": "United Kingdom" } ], "genres": [ { "slug": "action", "name_en": "Action" }, { "slug": "drama", "name_en": "Drama" }, { "slug": "mystery", "name_en": "Mystery" }, { "slug": "speculative_fiction", "name_en": "Speculative Fiction" }, { "slug": "thriller", "name_en": "Thriller" } ] } ], "limit": 1, "offset": 0, "total": null } ``` `has_more: true` means there are more results. The `offset` parameter returns the next page. ## Next - [Keys and limits](https://kinodata.space/en/docs/keys.md): plan limits and how to watch your balance. - [Title card](https://kinodata.space/en/docs/titles/card.md): every block of the card. - [Filters and sorting](https://kinodata.space/en/docs/filters.md): how to pick titles from the catalog. --- # Connect AI assistants > The kinodata MCP server: setup for Claude, Claude Code, Cursor and VS Code, the tools and how they use your quota. > https://kinodata.space/en/docs/mcp.md Connect kinodata to Claude, Cursor or VS Code, and the assistant finds a movie, links its IDs across every catalog and opens its title card whenever a conversation or your code needs it. The API has an MCP server for this: it speaks the open Model Context Protocol, which AI assistants use to call external tools. The server address: ```text https://api.kinodata.space/mcp ``` The MCP server is on every plan, Free included. It takes the same API key as the API — the one from your account. ## How requests are counted Every tool on the server makes ordinary requests to API v1.3 with your key. So the API and MCP share one daily quota, one rate limit and one usage chart in your account: - most tools cost one request; - `resolve_ids` costs one request per ID plus one for the whole batch; - `get_person` costs two: the person card and the first page of the filmography; - connecting and listing the tools cost nothing. The server keeps to the per-second limit itself: your account's requests go no faster than your plan allows, from all its keys, whether they come from one call or from several at once. So `resolve_ids` with 50 IDs on Free (10 requests a second) takes about five seconds but never breaks off halfway. When the daily quota runs out, the tool answers in plain text: how many requests were used and when the quota resets. The assistant passes this on by itself. Plan limits are on the [Keys and limits](https://kinodata.space/en/docs/keys.md) page. ## Connect your client The server takes the key in an `Authorization: Bearer ` header or, like the API, in `X-API-Key`. Each setup below reads the key from the `KINODATA_KEY` environment variable, as in the [Quickstart](https://kinodata.space/en/docs/quickstart.md). ### Claude Code macOS and Linux: ```bash claude mcp add --transport http kinodata https://api.kinodata.space/mcp --scope user --header "Authorization: Bearer $KINODATA_KEY" ``` Windows, PowerShell: ```powershell claude mcp add --transport http kinodata https://api.kinodata.space/mcp --scope user --header "Authorization: Bearer $env:KINODATA_KEY" ``` Where: - `kinodata` is the server's name in Claude Code. - `--scope user` makes the server available in all your projects and keeps it out of project files. Check the connection with `/mcp` in Claude Code. ### Cursor Open `~/.cursor/mcp.json` and add the server: ```json { "mcpServers": { "kinodata": { "url": "https://api.kinodata.space/mcp", "headers": { "Authorization": "Bearer ${env:KINODATA_KEY}" } } } } ``` Cursor fills in the key from the `KINODATA_KEY` environment variable. Cursor itself must see the variable, so set it system-wide, not only in an open terminal. ### VS Code Run **MCP: Open User Configuration** and add this to the `mcp.json` it opens: ```json { "inputs": [ { "type": "promptString", "id": "kinodata-key", "description": "kinodata API key", "password": true } ], "servers": { "kinodata": { "type": "http", "url": "https://api.kinodata.space/mcp", "headers": { "Authorization": "Bearer ${input:kinodata-key}" } } } } ``` VS Code asks for the key the first time the server starts, and the key never lands in the file. ### Claude In claude.ai and the Claude apps, add the server as a custom connector: 1. Open **Customize → Connectors** and click **Add custom connector**. 2. Enter `https://api.kinodata.space/mcp` as the server URL. 3. Under **Authentication**, choose **No sign-in**. 4. Under **Request headers**, add `authorization` with the value `Bearer ` followed by your key — the whole value, including the word `Bearer` and the space. **Request headers** is in beta and not every account has it yet. If you don't see it, connect the server in Claude Code, or in the Claude desktop app through the `mcp-remote` bridge (needs Node.js). In `claude_desktop_config.json`: ```json { "mcpServers": { "kinodata": { "command": "npx", "args": ["-y", "mcp-remote", "https://api.kinodata.space/mcp", "--header", "Authorization:${AUTH_HEADER}"], "env": { "AUTH_HEADER": "Bearer your_key" } } } } ``` There are no spaces around the colon in `args` on purpose: on Windows the app doesn't escape spaces in arguments, and the header breaks. A space inside `AUTH_HEADER` is fine. ### ChatGPT ChatGPT can't connect yet: it doesn't send a key in a header, and the server has no OAuth sign-in. ## Tools | tool | what it does | |---|---| | `resolve_ids` | A title's IDs in every catalog, up to 50 IDs at once. | | `search_titles` | Movies and series by title in Russian or English, typos tolerated. | | `get_title` | A title card: titles, year, genres, countries, ratings, overview, directors and leading cast. | | `get_title_section` | One long section of a title, page by page: cast, crew, awards, episodes, facts, quotes, media, sites, tags. | | `find_titles` | Titles by catalog conditions: kind, genres, countries, years, rating and vote count, a Kinorium tag, presets. | | `get_related_titles` | Similar titles and the franchise. | | `search_persons` | Actors, directors and other film people by name. | | `get_person` | A person card — dates, age, height, family with the relatives' gender, awards by ceremony, trivia, links (the catalogs, Wikipedia, Wikidata, their website and social networks) — and their filmography, page by page. | | `get_collection` | Collections: the list of them, or the titles of one, such as `daily-trending-titles` — trending today. | Answers are compact: the server leaves out empty fields and returns long lists in pages. ## What to ask - "Give me the Kinopoisk IDs for tt0111161, tt0068646 and tt0468569." - "Korean thrillers from the 2010s rated above 7.5 on Kinopoisk." - "Who directed *Stalker*, and what else did he make after 1970?" - "What's trending today, and is there any anime in it?" - "How many seasons and episodes does *Breaking Bad* have?" ## Keep your key safe - Keep the key in the client's user settings or in an environment variable. Don't put it in a project `mcp.json` that goes into a repository, and don't paste it into a chat with the assistant. - Send the key in a header only. The server rejects an address with the key in the query string. - Overviews and facts from the catalogs are third-party text. The server strips links, images and HTML from them, but if the assistant ever suggests an action "from a movie's overview", don't agree to it. ## If it doesn't connect | response | when | what to do | |---|---|---| | `401` | The key is missing, wrong or revoked. | Check the header: `Authorization: Bearer` and the whole key from your account. | | `400` | The key was sent in the address. | Remove it from the address and send it in a header. | | `429` | 30 requests with a wrong key came from your address within a minute. | Wait a minute and check the key. | ## Next - [Keys and limits](https://kinodata.space/en/docs/keys.md) — plan limits and how to watch your balance. - [IDs in every catalog](https://kinodata.space/en/docs/ids.md) — the endpoint behind `resolve_ids`. --- # Keys and limits > How to send the key, plan limits, balance headers and how to save requests. > https://kinodata.space/en/docs/keys.md Every request carries your API key in the `X-API-Key` header. Two limits apply: requests per second and requests per day. This page covers how to send the key, how to watch what you have left, and how to avoid wasting requests. ## Send the key Put the key in the `X-API-Key` header of every request. Every example in these docs reads it from the `KINODATA_KEY` environment variable: ```bash curl -H "X-API-Key: $KINODATA_KEY" \ "https://api.kinodata.space/v1.3/ids/kp/447301" ``` Response `200`: ```json { "imdb_id": "tt1375666", "kp_id": "447301", "tmdb_id": "27205", "tmdb_tv_id": null, "kinorium_id": "472809" } ``` Without a key, or with a wrong one, the API responds with `401`. The same request without the header: ```bash curl -s "https://api.kinodata.space/v1.3/ids/kp/447301" ``` Response `401`: ```json { "error": { "code": "unauthorized", "message": "missing api key" } } ``` The only address that works without a key is `https://api.kinodata.space/health`, a check that the API is up. ## Plan limits | plan | requests per second | requests per day | |---|---|---| | Free | 10 | 500 | | Basic | 20 | 5,000 | | Business | 40 | unlimited under fair use | Basic and Business let you issue two keys. The limits stay per account, not per key: two Basic keys share one 20 requests per second and 5,000 a day rather than getting that much each. Days are counted in UTC: the daily quota resets at midnight UTC, not 24 hours after your first request. Plans and prices are on the [Pricing](https://kinodata.space/en/pricing) page. ## Watch what's left Every response carries headers with your current balance: | header | what it is | |---|---| | `X-RateLimit-Limit` | Requests allowed per second. | | `X-RateLimit-Remaining` | Requests left in the current second. | | `X-RateLimit-Reset` | When the next second starts, as Unix time. | | `X-Quota-Limit` | Requests allowed per day. | | `X-Quota-Remaining` | Requests left today. | | `X-Quota-Reset` | When the quota resets, as Unix time: the next midnight UTC. | | `Retry-After` | Only on `429`: how many seconds to wait before retrying. | This is what the headers look like for a Basic key: ```text X-RateLimit-Limit: 20 X-RateLimit-Remaining: 19 X-RateLimit-Reset: 1791566201 X-Quota-Limit: 5000 X-Quota-Remaining: 4812 X-Quota-Reset: 1791590400 ``` A key without a limit doesn't get the matching headers. Browsers can read them too: the API lists them in `Access-Control-Expose-Headers`. ## When you hit a limit The API responds with `429`. The message says which limit you hit: ```json { "error": { "code": "rate_limited", "message": "quota limit exceeded" } } ``` - `rate limit exceeded`: too many requests in this second. Wait `Retry-After` seconds and spread your requests out; a queue or a pause between requests works better than retries. - `quota limit exceeded`: the daily quota is used up. `Retry-After` shows the seconds until midnight UTC. If you run out regularly, have a look at the [plans](https://kinodata.space/en/pricing). A request rejected by the per-second limit doesn't count against the daily quota. ## Save requests ### Ask only for the blocks you need A full title card is about 26 KB. If you need the title and the ratings, ask for just those: `fields=title,ratings`. The response gets smaller, and so does the load on your network. More in [Lists and pagination](https://kinodata.space/en/docs/lists.md#only-the-fields-you-need). ### Cache responses You can keep a response for five minutes: the API sends `Cache-Control: private, max-age=300`. Your server or the browser may store it, but a shared cache such as a CDN may not: the response belongs to the key. To check whether the data has changed without downloading it again, use a conditional request. 1. Save the `ETag` header from the response: ```bash ETAG=$(curl -sI -H "X-API-Key: $KINODATA_KEY" "https://api.kinodata.space/v1.3/titles/tmdb/27205" | grep -i '^etag:' | cut -d' ' -f2 | tr -d '\r') ``` 2. On the next request to the same URL, send it back in `If-None-Match` as is, quotes included: ```bash curl -s -o /dev/null -w '%{http_code}\n' -H "X-API-Key: $KINODATA_KEY" -H "If-None-Match: $ETAG" "https://api.kinodata.space/v1.3/titles/tmdb/27205" ``` If the data hasn't changed, you get `304` with no body, which means your saved response is still valid. A `304` doesn't count against the quota, so you can check for fresh data as often as you like. ### Turn on compression With the `Accept-Encoding: gzip` header the *Inception* card is 8 KB instead of 26 KB. Most HTTP clients ask for compression on their own; curl does it only with the `--compressed` flag. ## Keep the key on the server The API answers browser requests from any site, but a key in website or mobile app code is visible to anyone who opens the developer tools. Send requests from your own server and hand the browser ready data. ## When something goes wrong Every response carries two service headers: | header | what it is | |---|---| | `X-Request-Id` | The request ID. Include it when you contact us and we'll find your request. | | `X-API-Version` | The API version that answered: `1.3`. | ## Next - [Errors](https://kinodata.space/en/docs/errors.md): every code and what to do about it. - [Lists and pagination](https://kinodata.space/en/docs/lists.md): how to page through long responses. --- # Response languages > The language in the field name, the lang parameter and what is in one language only. > https://kinodata.space/en/docs/languages.md The API serves text in Russian and English. The language of every text field is in its name, and the `lang` parameter keeps just one language in the response. ## The language is in the field name Every text field ends in `_ru` or `_en`: `title_ru`, `kind_label_en`, `name_ru`. Without `lang` you get both halves: ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "fields=title,type" \ "https://api.kinodata.space/v1.3/titles/kp/447301" ``` Response `200`: ```json { "ids": { "imdb_id": "tt1375666", "kp_id": "447301", "tmdb_id": "27205", "tmdb_tv_id": null, "kinorium_id": "472809", "urls": { "imdb": "https://www.imdb.com/title/tt1375666/", "kinorium": "https://ru.kinorium.com/472809/", "kp": "https://www.kinopoisk.ru/film/447301/", "tmdb": "https://www.themoviedb.org/movie/27205" } }, "title": { "title_ru": "Начало", "title_en": "Inception", "title_original": "Inception" }, "type": { "kind": "movie", "is_series": false, "status": "released", "kind_label_ru": "Фильм", "kind_label_en": "Movie", "status_label_ru": "Вышел", "status_label_en": "Released" } } ``` Field names never change: `title_en` is always the English title, in every response and with any `lang`. Read the response by field name, not by the language you asked for. ## One language: lang `lang=en` keeps the `_en` fields, `lang=ru` keeps the `_ru` fields. Everything else in the response stays the same. ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "fields=title,type" \ --data-urlencode "lang=en" \ "https://api.kinodata.space/v1.3/titles/kp/447301" ``` Response `200`: ```json { "ids": { "imdb_id": "tt1375666", "kp_id": "447301", "tmdb_id": "27205", "tmdb_tv_id": null, "kinorium_id": "472809", "urls": { "imdb": "https://www.imdb.com/title/tt1375666/", "kinorium": "https://en.kinorium.com/472809/", "kp": "https://www.kinopoisk.ru/film/447301/", "tmdb": "https://www.themoviedb.org/movie/27205" } }, "title": { "title_en": "Inception", "title_original": "Inception" }, "type": { "kind": "movie", "is_series": false, "status": "released", "kind_label_en": "Movie", "status_label_en": "Released" } } ``` `lang` accepts only `ru` and `en`. Any other value returns `400`. You can send `lang` with any request: where a response has no text (`/ids`, `/meta/stats`), it changes nothing. ## No translation means null When there is no text in the requested language, the field is `null`. The API never falls back to another language or the original: what to show instead is your call. For a title, `title_original` is a sensible choice; for an overview, the text in the other language with a note. ## What doesn't depend on language Fields without a suffix are the same in every language: IDs, `slug`, `kind`, country codes, dates, numbers, `title_original`, company names. In date filters the `_ru` suffix means the region, not the language: `premiere_ru` is the premiere in Russia. ## What is in one language only today Both languages cover titles, overviews, genres, countries, mood tags and keywords, award ceremonies and categories, collections and the labels of every enum. Still in one language: | what | language | what you see | |---|---|---| | Facts, FAQ and quotes: `/titles/{type}/{id}/texts` | Russian | `text_en` is `null` | | A person's biography and birthplace: the [person card](https://kinodata.space/en/docs/persons/card.md) | English, from TMDB; only a handful of Russian biographies | `biography_ru` and `birthplace_ru` are almost always `null` | | Awards summary | two different texts: `summary_kinorium_ru` from Kinorium and `summary_imdb_en` from IMDb | not translations of each other | | Franchise name: `franchise.name` | English | the field has no suffix yet | With `lang=en`, facts come back without text, which shows the translation isn't there yet: ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "kind=fact" \ --data-urlencode "limit=1" \ --data-urlencode "lang=en" \ "https://api.kinodata.space/v1.3/titles/kp/447301/texts" ``` Response `200`: ```json { "counts": { "fact": 37, "faq": 16, "quote": 5 }, "has_more": true, "items": [ { "fact_kind": "general", "is_spoiler": false, "kind": "fact", "ord": 0, "text_en": null } ], "limit": 1, "offset": 0, "total": 37 } ``` When a translation arrives it lands in `*_en`, and your code doesn't need to change. ## English titles and posters - `title_en` holds only a real English title. The original title of a non-English film or a transliteration never goes there: in that case `title_en` is `null` and the original-language title is in `title_original`. - `poster_url_en` is a poster with English lettering; `null` if there is none. `poster_url_ru` is the poster for a Russian storefront. - With `lang=en`, Kinorium links point to the English site, `en.kinorium.com`. ## Next - [Lists and pagination](https://kinodata.space/en/docs/lists.md): how every list in the API works. - [IDs in every catalog](https://kinodata.space/en/docs/ids.md): one title's linked IDs. --- # Lists and pagination > The shape of every list, pages and cursors, sections and choosing fields. > https://kinodata.space/en/docs/lists.md Every list in the API has the same shape: the catalog and search, charts, collections, cast, media, filmographies. Learn it once and you can read any list. ## The shape of a list ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "q=matrix" \ --data-urlencode "limit=3" \ --data-urlencode "fields=title,year,rating_imdb" \ --data-urlencode "lang=en" \ "https://api.kinodata.space/v1.3/titles" ``` Response `200`: ```json { "has_more": true, "items": [ { "imdb_id": "tt0133093", "kp_id": "301", "tmdb_id": "603", "tmdb_tv_id": null, "kinorium_id": "116780", "title_en": "The Matrix", "year": 1999, "rating_imdb": 8.7 }, { "imdb_id": "tt0234215", "kp_id": "299", "tmdb_id": "604", "tmdb_tv_id": null, "kinorium_id": "190315", "title_en": "The Matrix Reloaded", "year": 2003, "rating_imdb": 7.2 }, { "imdb_id": "tt0242653", "kp_id": "316", "tmdb_id": "605", "tmdb_tv_id": null, "kinorium_id": "196883", "title_en": "The Matrix Revolutions", "year": 2003, "rating_imdb": 6.7 } ], "limit": 3, "offset": 0, "total": null } ``` | field | what it is | |---|---| | `items` | The items of the list. | | `total` | How many items there are in total. `null` when the API doesn't count them; see below. | | `limit` | Items per page. If you asked for more than allowed, this is the real number. | | `offset` | How many items were skipped from the start. | | `has_more` | Whether there is a next page. | | `next_cursor` | The cursor for the next page. Only when you page with a cursor. | The resource's own fields come alongside: `roles` on cast and filmographies, `counts` on relations and texts, `collection` on a collection. ## When total is null There are two kinds of lists. - **The set is defined by the URL**: a title's cast, its relations and media, a person's filmography, a collection. Such a set is finite, and `total` is a number. - **The set is defined by the query**: the catalog and search, the top chart, hidden gems, people search. Here `total` is `null`: an exact count of a filtered set would mean a second pass over the whole database on every page. Use `has_more` to tell whether there's more. Rating divergence and the release calendar do count `total`, filters included. ## Items per page | list | default | maximum | |---|---|---| | Catalog and search, charts, discovery feeds, release calendar, collections, people search | 50 | 50 | | Cast, media, texts, relations, episodes, filmography, shared titles | 1000 | 1000 | | Sections, when a list is split into them | 10 per section | 50 per section | A set defined by the URL comes back whole: it is the answer. A set defined by the query is sorted, and a page holds its top. `limit` is a whole number from 1, `offset` from 0. A number above the maximum isn't an error: the API lowers it and returns the real value in `limit`. `limit=5000` on the catalog gives `"limit": 50`. Zero, a negative number or a non-number returns `400`. ## Pages: offset ```text ?limit=50&offset=100 ``` `offset` is how many items to skip. The next page is `offset` plus `limit`, for as long as `has_more` is `true`. ## Deep paging: cursor For long runs through the catalog, `/titles`, use a cursor: it doesn't drift when the database changes while you page. Send an empty `cursor=` and the response gets a `next_cursor`: ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "sort=-rating_imdb" \ --data-urlencode "votes_imdb.gte=100000" \ --data-urlencode "limit=2" \ --data-urlencode "fields=title,rating_imdb" \ --data-urlencode "cursor=" \ "https://api.kinodata.space/v1.3/titles" ``` Response `200`: ```json { "has_more": true, "items": [ { "imdb_id": "tt0903747", "kp_id": "404900", "tmdb_id": null, "tmdb_tv_id": "1396", "kinorium_id": "400796", "title_ru": "Во все тяжкие", "title_en": "Breaking Bad", "rating_imdb": 9.5 }, { "imdb_id": "tt5491994", "kp_id": "1007472", "tmdb_id": null, "tmdb_tv_id": "68595", "kinorium_id": "1605088", "title_ru": "Планета Земля 2", "title_en": "Planet Earth II", "rating_imdb": 9.4 } ], "limit": 2, "next_cursor": "eyJzIjoicmF0aW5nX2ltZGIiLCJkIjp0cnVlLCJ2IjoiOS40MCIsImlkIjoyMjA4MDF9", "total": null } ``` Pass `next_cursor` to the next request with the same parameters until it comes back `null`. The cursor works in the `/titles` catalog and in the feeds built on it, the top chart and hidden gems. Other lists page with `offset` and answer `cursor` with `400`. `sort=random` can't be paged: a random order has no next page. `cursor` and `offset` with it return `400`; just repeat the request for a fresh draw. ## Sections Lists whose items differ by one property come split into sections. Each section is a list itself, with the same fields and a label in both languages: ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "limit=1" \ --data-urlencode "fields=title" \ "https://api.kinodata.space/v1.3/titles/kp/301/relations" ``` Response `200`: ```json { "counts": { "reference": 86, "referenced_in": 672, "related": 9, "similar": 74 }, "has_more": false, "items": [ { "has_more": true, "items": [ { "imdb_id": "tt1375666", "kp_id": "447301", "tmdb_id": "27205", "tmdb_tv_id": null, "kinorium_id": "472809", "title_ru": "Начало", "title_en": "Inception" } ], "label_en": "Similar", "label_ru": "Похожие", "relation": "similar", "total": 74 }, { "has_more": true, "items": [ { "imdb_id": "tt0366179", "kp_id": "63744", "tmdb_id": "24358", "tmdb_tv_id": null, "kinorium_id": null, "title_ru": "Аниматрица: Второе возрождение – Часть 1", "title_en": "The Second Renaissance Part I" } ], "label_en": "Related", "label_ru": "Связанные", "relation": "related", "total": 9 }, { "has_more": true, "items": [ { "imdb_id": "tt1431045", "kp_id": "462360", "tmdb_id": "293660", "tmdb_tv_id": null, "kinorium_id": "485426", "title_ru": "Дэдпул", "title_en": "Deadpool" } ], "label_en": "Referenced in", "label_ru": "Упоминается в", "relation": "referenced_in", "total": 672 }, { "has_more": true, "items": [ { "imdb_id": "tt0060196", "kp_id": "349", "tmdb_id": "429", "tmdb_tv_id": null, "kinorium_id": "56474", "title_ru": "Хороший, плохой, злой", "title_en": "The Good, the Bad and the Ugly" } ], "label_en": "References", "label_ru": "Отсылки к", "relation": "reference", "total": 86 } ], "limit": 1, "offset": 0, "total": 4 } ``` | list | default sections | other `group` values | |---|---|---| | Cast, `/titles/{type}/{id}/cast` | by role: `role` | `none` | | Texts, `/titles/{type}/{id}/texts` | by kind: `kind` | `none` | | Relations, `/titles/{type}/{id}/relations` | by relation: `relation` | `none` | | Media, `/titles/{type}/{id}/media` | no sections | `type`, `class`, `language` | | Filmography, `/persons/{type}/{id}/filmography` | no sections | `role` | | Episodes, `/titles/{type}/{id}/episodes` | no sections | `season`: a per-season summary | In a sectioned list `limit` applies to each section, and a section's `total` is its real size. For a flat list pass `group=none`. To page through one section, narrow the list to it: `role=actor`, `kind=fact`, `relation=similar`. A sectioned list can't be paged: the sections share no single sequence. `offset` without narrowing returns `400`: ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "limit=2" \ --data-urlencode "offset=2" \ "https://api.kinodata.space/v1.3/titles/kp/447301/cast" ``` Response `400`: ```json { "error": { "code": "invalid_request", "message": "grouped lists have no single sequence to page through — narrow to one value to page inside it, or ask for group=none", "field": "offset" } } ``` ## Only the fields you need The `fields` parameter keeps only what you list. - **On a title card** the names are card blocks: `fields=title,ratings`. You can also drop what you don't need: `fields=-akas,-tags`. The two can't be mixed in one parameter: `fields=title,-akas` returns `400`. - **In lists** the names are list item fields: `fields=title,year,rating_imdb` or `fields=-genres,-countries`. `title` keeps `title_ru` and `title_en`, `poster_url` keeps `poster_url_ru` and `poster_url_en`. A title's IDs always come back, even if you didn't list them: without them there's nothing to link the item to. Fields keep the same order as without `fields`. An unknown name returns `400` with the list of allowed names. `fields` works on the title card, in every list of titles (the catalog, collections, relations, filmographies, shared titles, discovery feeds and the release calendar) and in people search. Cast, media, texts and episodes don’t take it: there `fields` returns `400`. ## Next - [Filters and sorting](https://kinodata.space/en/docs/filters.md): how to pick and order items. - [Catalog and search](https://kinodata.space/en/docs/titles.md): the main list in the API. --- # Filters and sorting > Operators, every catalog filter, sorting, the rating window and presets. > https://kinodata.space/en/docs/filters.md The `/titles` catalog picks titles with filters and orders them with `sort`. The same grammar works in collections, relations, filmographies, shared titles and discovery feeds, and the filters work in the release calendar too: learn it once and you can filter any list of titles. ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "kind=anime" \ --data-urlencode "country=jp" \ --data-urlencode "votes_imdb.gte=50000" \ --data-urlencode "sort=-rating_imdb" \ --data-urlencode "limit=3" \ --data-urlencode "fields=title,year,rating_imdb" \ "https://api.kinodata.space/v1.3/titles" ``` Response `200`: ```json { "has_more": true, "items": [ { "imdb_id": "tt2560140", "kp_id": "749374", "tmdb_id": null, "tmdb_tv_id": "1429", "kinorium_id": "678289", "title_ru": "Атака титанов", "title_en": "Attack on Titan", "year": 2013, "rating_imdb": 9.1 }, { "imdb_id": "tt1355642", "kp_id": "452838", "tmdb_id": null, "tmdb_tv_id": "31911", "kinorium_id": "468024", "title_ru": "Стальной алхимик: Братство", "title_en": "Fullmetal Alchemist: Brotherhood", "year": 2009, "rating_imdb": 9.1 }, { "imdb_id": "tt14986406", "kp_id": "4789626", "tmdb_id": null, "tmdb_tv_id": "326913", "kinorium_id": "2794509", "title_ru": "Блич: Тысячелетняя кровавая война", "title_en": "Bleach: Thousand-Year Blood War", "year": 2022, "rating_imdb": 9 } ], "limit": 3, "offset": 0, "total": null } ``` Where: - `kind=anime` and `country=jp` select Japanese anime; - `votes_imdb.gte=50000` asks for at least 50,000 IMDb votes; - `sort=-rating_imdb` orders from the highest IMDb rating down. ## Operators An operator goes after the filter name: | operator | meaning | example | |---|---|---| | none | equals; several comma-separated values mean any of them | `kind=movie,series` | | `.any` | any of the listed values | `genre.any=drama,comedy` | | `.all` | all of the listed values at once | `genre.all=drama,war` | | `.not` | none of the listed values | `country.not=us` | | `.gte`, `.lte` | at least, at most | `year.gte=1990&year.lte=1999` | A number without an operator is an exact value: `year=1999` is the same as `year.gte=1999&year.lte=1999`. Conditions on different filters combine with "and". ## What you can filter | group | filters | |---|---| | IDs | `imdb_id`, `kp_id`, `tmdb_id`, `tmdb_tv_id` | | Kind | `kind`, `is_series`, `adult` | | Dates | `year`, `release_date`, `premiere_world`, `premiere_ru`, `digital_release`, `released`, `start_year`, `end_year` | | Ratings and votes | `rating`, `rating_imdb`, `rating_kp`, `rating_tmdb`, `rating_kr`, `rating_letterboxd`, `rating_rt`, `rating_metacritic`, `rating_critics`, `votes_imdb`, `votes_kp`, `votes_tmdb`, `votes_kr`, `votes_letterboxd`, `votes_total`, `popularity` | | Content | `genre`, `tag`, `country`, `region`, `original_language`, `runtime` | | Companies | `companies`, `company.kind`, `platform` | | People | `person`, `person.role` | | Collections and franchises | `collection`, `franchise` | | Series | `seasons_total`, `episodes_total` | | Money | `budget`, `box_office_us`, `revenue_world` | | Awards | `wins_imdb`, `oscars_won` | | Age rating | `age_limit`, `age_limit_max`, `mpaa` | | Data completeness | `required`, `has_poster` | [`GET /filters`](https://kinodata.space/en/docs/facets.md) returns the values for `genre`, `country`, `region`, `platform`, `tag` and `companies` with title counts, in the `value` field. [`GET /enums`](https://kinodata.space/en/docs/enums.md) returns the labels of every enum, the `sort` keys and the `fields` names. ## Sorting ```text ?sort=-rating_imdb descending ?sort=+year ascending ?sort=-rating_imdb,+year the second key breaks ties in the first ``` The sign is the direction; no sign means descending. Keys: `popularity`, `relevance`, `release_date`, `year`, `runtime`, `rating`, `rating_imdb`, `rating_kp`, `rating_tmdb`, `rating_kr`, `rating_letterboxd`, `rating_rt`, `rating_metacritic`, `rating_critics`, `votes_imdb`, `votes_kp`, `votes_tmdb`, `votes_kr`, `votes_letterboxd`, `votes_total`, `rating_weighted`, `divergence`, `budget`, `box_office_us`, `revenue_world`, `wins_imdb`, `oscars_won`, `random`. The money and award names match the title card. You can send the `+` sign as is: `sort=+year` and `sort=%2Byear` work the same. - **Without `sort`** the catalog goes by popularity: the vote total across all sources. Titles without votes, announcements included, come last. - **With a search `q`** the default is relevance. - **Sorting by a field** keeps only titles that have it. Collections, relations and filmographies are the exception: there, titles without the value stay at the end. - **`sort=rating`** takes a title's first known rating (IMDb, then Kinopoisk, then TMDB) and puts titles with 50 or more votes above the rest. - **`sort=rating_weighted`** is the average of the Kinopoisk, IMDb and TMDB ratings, weighted by vote count: a rating more people stand behind weighs more. - **`sort=divergence`** orders by the gap between the Kinopoisk and IMDb ratings: with no sign or with `-`, largest gaps first; `+divergence` puts the closest ratings first. More in [Rating divergence](https://kinodata.space/en/docs/discover/divergence.md). - **`sort=popularity`** orders by the vote total across all sources. The `popularity` filter and the card's `ratings.popularity` are a different number: TMDB's popularity index. - **`sort=random`** shuffles the set. It can't be paged; repeat the request for a fresh draw. ## Ratings: set a vote threshold The card has no overall score on purpose: sources have different scales and audiences, and an average would hide that. For sorting there is `rating_weighted`, an average weighted by vote count, but set a threshold with it too. Sorting by a single rating without a vote threshold lifts titles rated by a handful of people to the top: IMDb and TMDB accept votes before release, so even an unreleased film can get a 10 from one viewer. Set the threshold explicitly: ```text ?rating_imdb.gte=8&votes_imdb.gte=100000&sort=-rating_imdb ``` Every source with votes has a threshold: `votes_imdb`, `votes_kp`, `votes_tmdb`, `votes_kr` (Kinorium), `votes_letterboxd`. ### The rating window When several audiences need to agree, use the window: ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "rating_min=8.5" \ --data-urlencode "rating_sources=imdb,kp" \ --data-urlencode "rating_match=all" \ --data-urlencode "votes_imdb.gte=500000" \ --data-urlencode "sort=-votes_imdb" \ --data-urlencode "limit=3" \ --data-urlencode "fields=title,rating_imdb,rating_kp" \ "https://api.kinodata.space/v1.3/titles" ``` Response `200`: ```json { "has_more": true, "items": [ { "imdb_id": "tt0468569", "kp_id": "111543", "tmdb_id": "155", "tmdb_tv_id": null, "kinorium_id": "352641", "title_ru": "Тёмный рыцарь", "title_en": "The Dark Knight", "rating_kp": 8.5, "rating_imdb": 9.1 }, { "imdb_id": "tt0111161", "kp_id": "326", "tmdb_id": "278", "tmdb_tv_id": null, "kinorium_id": "101209", "title_ru": "Побег из Шоушенка", "title_en": "The Shawshank Redemption", "rating_kp": 9.1, "rating_imdb": 9.3 }, { "imdb_id": "tt1375666", "kp_id": "447301", "tmdb_id": "27205", "tmdb_tv_id": null, "kinorium_id": "472809", "title_ru": "Начало", "title_en": "Inception", "rating_kp": 8.7, "rating_imdb": 8.8 } ], "limit": 3, "offset": 0, "total": null } ``` | parameter | meaning | |---|---| | `rating_min`, `rating_max` | The window's bounds. | | `rating_sources` | Whose ratings to check: `kp`, `imdb`, `tmdb`, `kr` (Kinorium), `letterboxd`. The first three by default. | | `rating_match` | `all`: every listed source must fall in the window. Without it, any one is enough. | ## People and companies `person` picks titles a person worked on. Pass the ID with its catalog and a colon: `kp:41477`, `imdb:nm0634240`, `tmdb:525`, `kinorium:476400`. Several people separated by commas means any of them; repeating the parameter means all of them: `person=kp:37859&person=kp:41477` gives titles with both DiCaprio and Nolan. `person.role` narrows the role and returns `400` without `person`: ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "person=kp:41477" \ --data-urlencode "person.role=director" \ --data-urlencode "released=true" \ --data-urlencode "sort=-year" \ --data-urlencode "limit=3" \ --data-urlencode "fields=title,year" \ "https://api.kinodata.space/v1.3/titles" ``` Response `200`: ```json { "has_more": true, "items": [ { "imdb_id": "tt33764258", "kp_id": "6385370", "tmdb_id": "1368337", "tmdb_tv_id": null, "kinorium_id": "11772913", "title_ru": "Одиссея", "title_en": "The Odyssey", "year": 2026 }, { "imdb_id": "tt15398776", "kp_id": "4664634", "tmdb_id": "872585", "tmdb_tv_id": null, "kinorium_id": "2828885", "title_ru": "Оппенгеймер", "title_en": "Oppenheimer", "year": 2023 }, { "imdb_id": "tt6723592", "kp_id": "1236063", "tmdb_id": "577922", "tmdb_tv_id": null, "kinorium_id": "2023303", "title_ru": "Довод", "title_en": "Tenet", "year": 2020 } ], "limit": 3, "offset": 0, "total": null } ``` `companies` searches by company name, `company.kind` by the company's role: `production`, `studio`, `network`, `special_effects`, `distributor_theatrical`, `distributor_online`, `dubbing`. Together they answer "where was HBO the network": `companies=HBO&company.kind=network`. ## Age ratings and release - `age_limit` is the Russian age rating: 0, 6, 12, 16, 18. A comma-separated list (`age_limit=12,16`) or a range (`age_limit.gte=16`). `age_limit_max=12` is the same as `age_limit.lte=12`. Titles without a rating never match: an unknown rating isn't zero. - `mpaa` is the US certificate: `G`, `PG`, `PG-13`, `R`, `NC-17` and the `TV-*` family. A title has one certificate, so for "any of" write `mpaa=R,NC-17`. - `released=true` is out already, `released=false` is upcoming. A title counts as upcoming if its release date or year is later than today. ## Ready-made sets: preset A `preset` is a named set of filters. Explicit parameters refine it: `preset=top&votes_imdb.gte=500000` is stricter than the usual top. `order=asc` reverses the preset’s sort (`preset=top&order=asc` starts with the lowest ratings), while `sort` replaces it entirely. | preset | what's inside | |---|---| | `top` | `sort=-rating_imdb`, `votes_imdb.gte=100000`, has a Kinopoisk rating and a poster | | `hidden_gems` | everything but shorts and TV shows: 1,000–50,000 Kinopoisk votes and 1,000–25,000 IMDb votes, no concerts, stand-up, stage recordings, music or documentaries, `sort=-rating_weighted` | | `trending` | released, `sort=-popularity` | | `new` | released in the last 30 days, `sort=-release_date` | | `classics` | up to 1980, 50,000+ IMDb votes, `sort=-rating_imdb` | ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "preset=top" \ --data-urlencode "limit=3" \ --data-urlencode "fields=title,rating_imdb" \ "https://api.kinodata.space/v1.3/titles" ``` Response `200`: ```json { "has_more": true, "items": [ { "imdb_id": "tt0903747", "kp_id": "404900", "tmdb_id": null, "tmdb_tv_id": "1396", "kinorium_id": "400796", "title_ru": "Во все тяжкие", "title_en": "Breaking Bad", "rating_imdb": 9.5 }, { "imdb_id": "tt5491994", "kp_id": "1007472", "tmdb_id": null, "tmdb_tv_id": "68595", "kinorium_id": "1605088", "title_ru": "Планета Земля 2", "title_en": "Planet Earth II", "rating_imdb": 9.4 }, { "imdb_id": "tt0795176", "kp_id": "279548", "tmdb_id": null, "tmdb_tv_id": "1044", "kinorium_id": "379535", "title_ru": "Планета Земля", "title_en": "Planet Earth", "rating_imdb": 9.4 } ], "limit": 3, "offset": 0, "total": null } ``` ## Collections and franchises - `collection` picks the titles of one of our collections by its `slug`: `collection=popular-titles`. The list of collections is `GET /collections`. An unknown collection returns `400` with the list of existing ones. - `franchise` picks a franchise's titles by part of its TMDB name: `franchise=matrix`. ## Data completeness `required` keeps titles that have every listed field filled: `required=poster,rating_imdb` or `required=kinorium_id`. `has_poster=true` does the same for the poster alone. ## In other lists The same filters and `sort` work in collections, relations, filmographies, shared titles and discovery feeds, and the filters work in the release calendar too. For example, Christopher Nolan's movies with 100,000 votes or more, by IMDb rating: ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "kind=movie" \ --data-urlencode "votes_imdb.gte=100000" \ --data-urlencode "sort=-rating_imdb" \ --data-urlencode "limit=3" \ --data-urlencode "fields=title,rating_imdb" \ "https://api.kinodata.space/v1.3/persons/kp/41477/filmography" ``` Response `200`: ```json { "has_more": true, "items": [ { "imdb_id": "tt0468569", "kp_id": "111543", "tmdb_id": "155", "tmdb_tv_id": null, "kinorium_id": "352641", "title_ru": "Тёмный рыцарь", "title_en": "The Dark Knight", "rating_imdb": 9.1 }, { "imdb_id": "tt1375666", "kp_id": "447301", "tmdb_id": "27205", "tmdb_tv_id": null, "kinorium_id": "472809", "title_ru": "Начало", "title_en": "Inception", "rating_imdb": 8.8 }, { "imdb_id": "tt0816692", "kp_id": "258687", "tmdb_id": "157336", "tmdb_tv_id": null, "kinorium_id": "384511", "title_ru": "Интерстеллар", "title_en": "Interstellar", "rating_imdb": 8.7 } ], "limit": 3, "offset": 0, "roles": [ { "role": "director", "label_ru": "Режиссёры", "label_en": "Directors", "count": 19 }, { "role": "actor", "label_ru": "Актёры", "label_en": "Actors", "count": 2 }, { "role": "self", "label_ru": "В роли себя", "label_en": "Self", "count": 90 }, { "role": "archive", "label_ru": "Архивные кадры", "label_en": "Archive Footage", "count": 12 }, { "role": "writer", "label_ru": "Сценаристы", "label_en": "Writers", "count": 20 }, { "role": "producer", "label_ru": "Продюсеры", "label_en": "Producers", "count": 22 }, { "role": "cinematographer", "label_ru": "Операторы", "label_en": "Cinematographers", "count": 5 }, { "role": "composer", "label_ru": "Композиторы", "label_en": "Composers", "count": 1 }, { "role": "editor", "label_ru": "Монтажёры", "label_en": "Editors", "count": 5 }, { "role": "crew", "label_ru": "Съёмочная группа", "label_en": "Crew", "count": 3 } ], "total": 18 } ``` Presets work too: `preset=top` in a filmography puts the best IMDb-rated titles first. In filmographies and shared titles the default order runs from newest to oldest, and `order=asc` reverses it. ## Mistakes in a request An unknown filter or operator returns `400` instead of being ignored: a silently dropped filter would look like a filter that everything matched. When the fix is obvious, the API suggests it in `hint`. There is no `status` filter; ask about release status with `released`: ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "status=released" \ "https://api.kinodata.space/v1.3/titles" ``` Response `400`: ```json { "error": { "code": "invalid_request", "message": "unknown query parameter \"status\"", "field": "status", "hint": "there is no status filter; the card carries type.status, the catalog filters by released" } } ``` A parameter that takes one value can’t be repeated: `kind=movie&kind=series` returns `400` too. Put several values in one parameter, separated by commas: `kind=movie,series`. The exception is `person`: repeating it means “all of these people”. ## Next - [Catalog and search](https://kinodata.space/en/docs/titles.md): every `/titles` parameter. - [Lists and pagination](https://kinodata.space/en/docs/lists.md): how to page through the result. --- # Errors > What an error looks like, what each code means and when to retry. > https://kinodata.space/en/docs/errors.md When a request fails, the API responds with a 4xx or 5xx status and an `error` object. The code says what happened; the fields next to it say which parameter is at fault and which values are allowed. Here the catalog type has a typo, `tmbd` instead of `tmdb`: ```bash curl -H "X-API-Key: $KINODATA_KEY" \ "https://api.kinodata.space/v1.3/ids/tmbd/27205" ``` Response `400`: ```json { "error": { "code": "invalid_request", "message": "unknown type \"tmbd\"", "field": "type", "allowed": [ "imdb", "kp", "tmdb", "tmdb_tv", "kinorium" ], "hint": "did you mean \"tmdb\"?" } } ``` ## Error fields | field | always present | what it is | |---|---|---| | `code` | yes | A machine-readable code. Branch on it. | | `message` | yes | A human-readable explanation in English. The wording may change, so don't parse it. | | `field` | no | The parameter that caused the rejection. | | `allowed` | no | The values that parameter accepts. | | `hint` | no | A suggestion when the value looks like an allowed one (`tmbd` → `tmdb`) or is part of one (`sort=kp` → `rating_kp` or `votes_kp`). | Branch on `code`, not on the status: one status can have several causes. ## Codes ### invalid_request - **Status:** 400. - **When:** an unknown parameter or value, an ID that doesn't match the catalog format, a repeated parameter, a `limit` that isn't a whole number from 1. - **What to do:** read `field`, `allowed` and `hint`, then fix the request. ### unauthorized - **Status:** 401. - **When:** the `X-API-Key` header is missing, or the key is unknown or revoked. - **What to do:** check that the header is named `X-API-Key` and that the whole key was copied. ### forbidden - **Status:** 403. - **When:** requests from your address are blocked because it was guessing keys or otherwise broke the rules. `message` is `this address is blocked`. A ban for guessing keys lifts by itself after a day. - **What to do:** if your address was blocked by mistake, for example because an office or a carrier shares it, [contact us](https://kinodata.space/en/contacts). ### not_found - **Status:** 404. - **When:** no title or person with this ID, no collection with this `slug`, or no such path. - **What to do:** check the type and the ID. `tmdb` and `tmdb_tv` are easy to mix up, and their IDs are different. ### rate_limited - **Status:** 429. - **When:** the per-second rate limit or the daily quota is used up. `message` says which: `rate limit exceeded` for the second, `quota limit exceeded` for the day. - **What to do:** wait the number of seconds in the `Retry-After` header. The daily quota resets at midnight UTC. - **How to stay under it:** every response shows what is left. `X-RateLimit-Remaining` is the requests left in the current second, `X-Quota-Remaining` is the requests left today, and `X-Quota-Reset` is when the quota resets (Unix time). Keys without limits don't get these headers. ### internal - **Status:** 500. - **When:** something failed on our side. - **What to do:** retry after a pause. If it keeps happening, contact us and include the `X-Request-Id`. ### source_unavailable - **Status:** 502. - **When:** a catalog we were filling data from did not respond. - **What to do:** retry later. ## Should you retry | code | retry | |---|---| | `rate_limited` | yes, after `Retry-After` seconds | | `internal`, `source_unavailable` | yes, with a growing pause: 1, 2, 4 seconds | | everything else | no: the request won't fix itself | Every request in the API is a `GET`, so a retry changes nothing and is safe. ## If you're stuck Every response carries an `X-Request-Id` header. Include it when you contact us and we'll find your request. ## Next - [Keys and limits](https://kinodata.space/en/docs/keys.md): how to stay clear of `429`. - [Lists and pagination](https://kinodata.space/en/docs/lists.md): the rules for `limit`, `offset` and `cursor`. --- # IDs in every catalog > Takes a title’s ID in one catalog and returns its IDs in the others. > https://kinodata.space/en/docs/ids.md Request: `GET https://api.kinodata.space/v1.3/ids/{type}/{id}` The lightest request in the API: IDs only, without titles or metadata. Use it when you need the IDs for your own database or to open the title on a catalog site. `tmdb` and `tmdb_tv` are separate ID spaces. The same number means different works in each: `/ids/tmdb/1396` is Tarkovsky’s *Mirror*, `/ids/tmdb_tv/1396` is *Breaking Bad*. Use the type TMDB itself gave you. ## Path parameters - `type` (string, required) — The catalog the ID comes from: IMDb, Kinopoisk, TMDB (movie or series) or Kinorium. Values: `imdb`, `kp`, `tmdb`, `tmdb_tv`, `kinorium`. - `id` (string, required) — The ID in that catalog. IMDb IDs are `tt` followed by digits (`tt1375666`); the others are digits (`447301`). ## Response fields - `imdb_id` (string | null) — IMDb ID: `tt` followed by digits. - `kp_id` (string | null) — Kinopoisk ID. - `tmdb_id` (string | null) — TMDB movie ID. `null` for a series: its ID is in `tmdb_tv_id`. - `tmdb_tv_id` (string | null) — TMDB series ID. At most one of `tmdb_id` and `tmdb_tv_id` is set. - `kinorium_id` (string | null) — Kinorium ID. ## Errors | status | code | when | |---|---|---| | 400 | `invalid_request` | Unknown `type`, or the ID does not match the catalog format: no `tt` for IMDb, not digits for the rest; a query parameter other than `lang`. | | 404 | `not_found` | No title found for this ID. | | 401 | `unauthorized` | The `X-API-Key` header is missing, or the key is unknown or revoked. | | 429 | `rate_limited` | The per-second rate limit or the daily quota is used up. | Every code and what to do about it is on the [Errors](https://kinodata.space/en/docs/errors.md) page. ## Examples ### By Kinopoisk ID ```bash curl -H "X-API-Key: $KINODATA_KEY" \ "https://api.kinodata.space/v1.3/ids/kp/447301" ``` Response `200`: ```json { "imdb_id": "tt1375666", "kp_id": "447301", "tmdb_id": "27205", "tmdb_tv_id": null, "kinorium_id": "472809" } ``` ### TMDB series ```bash curl -H "X-API-Key: $KINODATA_KEY" \ "https://api.kinodata.space/v1.3/ids/tmdb_tv/1396" ``` ### Unknown ID ```bash curl -H "X-API-Key: $KINODATA_KEY" \ "https://api.kinodata.space/v1.3/ids/kp/999999999" ``` ### Typo in type ```bash curl -H "X-API-Key: $KINODATA_KEY" \ "https://api.kinodata.space/v1.3/ids/tmbd/27205" ``` --- # Catalog and search > Search titles by name and pick them from the whole database with filters. > https://kinodata.space/en/docs/titles.md Request: `GET https://api.kinodata.space/v1.3/titles` The main list in the API: search, storefronts and selections by any property. Filters, sorting and presets are covered in [Filters and sorting](https://kinodata.space/en/docs/filters.md); paging and `fields` in [Lists and pagination](https://kinodata.space/en/docs/lists.md). Without `sort` the catalog goes by popularity, with titles that have no votes last. Pair sorting by a rating with a vote threshold: a handful of people can rate a title too. ## Query parameters - `q` (string) — Search by title in any language. Forgives typos: “matrx” finds *The Matrix*. With `q` the default order is relevance. Example: `q=матрица`. - `filters` — Any catalog filter with the `.any`, `.all`, `.not`, `.gte`, `.lte` operators: `kind=anime`, `country=jp`, `year.gte=1990`, `votes_imdb.gte=50000`, `person=kp:41477`. The full list is in [Filters and sorting](https://kinodata.space/en/docs/filters.md#what-you-can-filter). - `sort` (string) — Order: a key with a sign, `-` for descending, `+` for ascending. Two keys are allowed: `sort=-rating_imdb,+year`. Popularity by default, relevance with `q`. Values: `popularity`, `relevance`, `release_date`, `year`, `runtime`, `rating`, `rating_imdb`, `rating_kp`, `rating_tmdb`, `rating_kr`, `rating_letterboxd`, `rating_rt`, `rating_metacritic`, `rating_critics`, `votes_imdb`, `votes_kp`, `votes_tmdb`, `votes_kr`, `votes_letterboxd`, `votes_total`, `rating_weighted`, `divergence`, `budget`, `box_office_us`, `revenue_world`, `wins_imdb`, `oscars_won`, `random`. Default: `popularity`, `relevance` with `q`. Example: `sort=-rating_imdb`. - `preset` (enum) — A ready-made set of filters. Explicit parameters refine it. What each one holds is in [Filters and sorting](https://kinodata.space/en/docs/filters.md#ready-made-sets-preset). Values: `top`, `hidden_gems`, `trending`, `new`, `classics`. Example: `preset=top`. - `limit` (integer) — Titles per page, from 1 to 50. Default: 50. Example: `limit=20`. - `offset` (integer) — How many titles to skip from the start. Default: 0. - `cursor` (string) — A cursor for deep paging. Send an empty `cursor=` first, then the `next_cursor` value from the response. Doesn’t work with `sort=random`. Only the catalog has it. - `fields` (csv) — Which item fields to return: `fields=title,year`. With a `-` sign, which to drop: `fields=-genres`. `title` keeps `title_ru` and `title_en`, `poster_url` keeps `poster_url_ru` and `poster_url_en`. IDs always come back. Values: `title`, `title_original`, `year`, `release_date`, `kind`, `poster_url`, `posters_by_source`, `rating_kp`, `rating_tmdb`, `rating_imdb`, `rating_kr`, `rating_letterboxd`, `votes_kp`, `votes_tmdb`, `votes_imdb`, `countries`, `genres`. Example: `fields=title,year`. - `lang` (ru | en) — Keep one language in the response. ## Response fields - `items` (array) — Titles. - `imdb_id`, `kp_id`, `tmdb_id`, `tmdb_tv_id`, `kinorium_id` (string | null) — The title’s catalog IDs. Always returned, even with `fields`. - `title_ru`, `title_en`, `title_original` (string | null) — Titles: Russian, English and in the original language. - `year` (integer | null) — Release year. - `release_date` (string | null) — First release date, `YYYY-MM-DD`. - `kind` (enum) — Kind of title: `movie`, `series`, `mini_series`, `tv_show`, `anime`, `cartoon`, `short`. - `poster_url_ru`, `poster_url_en` (string | null) — Poster for a Russian and an English storefront. - `posters_by_source` (object) — Each source’s poster: `kp`, `tmdb`, `imdb`, `kinorium`. - `rating_kp`, `rating_tmdb`, `rating_imdb`, `rating_kr`, `rating_letterboxd` (number | null) — Kinopoisk, TMDB, IMDb, Kinorium and Letterboxd ratings, 0–10 scale. - `votes_kp`, `votes_tmdb`, `votes_imdb` (integer | null) — Kinopoisk, TMDB and IMDb votes. - `countries` (array) — Countries: `code`, `name_ru`, `name_en`. - `genres` (array) — Genres: `slug`, `name_ru`, `name_en`. - `total` (null) — The catalog doesn’t count the total: an exact count of a filtered set is a second pass over the whole database. Page by `has_more`. - `limit`, `offset` (integer) — Page size and offset. There is no `offset` when you page with a cursor. - `has_more` (boolean) — Whether there is a next page. - `next_cursor` (string | null) — The next page’s cursor, only if the request had `cursor`. `null` means there is nothing more. ## Errors | status | code | when | |---|---|---| | 400 | `invalid_request` | An unknown filter, operator, `sort` key or `fields` name; a repeated parameter other than `person`; an unknown collection in `collection`; a parameter of another endpoint (such as `role` or `relation`); a non-numeric `limit`; `person.role` without `person`; `cursor` or `offset` with `sort=random`; a malformed cursor. | | 401 | `unauthorized` | The `X-API-Key` header is missing, or the key is unknown or revoked. | | 429 | `rate_limited` | The per-second rate limit or the daily quota is used up. | Every code and what to do about it is on the [Errors](https://kinodata.space/en/docs/errors.md) page. ## Examples ### English only ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "q=matrix" \ --data-urlencode "limit=3" \ --data-urlencode "fields=title,year,rating_imdb" \ --data-urlencode "lang=en" \ "https://api.kinodata.space/v1.3/titles" ``` Response `200`: ```json { "has_more": true, "items": [ { "imdb_id": "tt0133093", "kp_id": "301", "tmdb_id": "603", "tmdb_tv_id": null, "kinorium_id": "116780", "title_en": "The Matrix", "year": 1999, "rating_imdb": 8.7 }, { "imdb_id": "tt0234215", "kp_id": "299", "tmdb_id": "604", "tmdb_tv_id": null, "kinorium_id": "190315", "title_en": "The Matrix Reloaded", "year": 2003, "rating_imdb": 7.2 }, { "imdb_id": "tt0242653", "kp_id": "316", "tmdb_id": "605", "tmdb_tv_id": null, "kinorium_id": "196883", "title_en": "The Matrix Revolutions", "year": 2003, "rating_imdb": 6.7 } ], "limit": 3, "offset": 0, "total": null } ``` ### With a typo ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "q=matrx" \ --data-urlencode "limit=3" \ --data-urlencode "fields=title,year,rating_imdb" \ --data-urlencode "lang=en" \ "https://api.kinodata.space/v1.3/titles" ``` ### Filters ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "kind=anime" \ --data-urlencode "country=jp" \ --data-urlencode "votes_imdb.gte=50000" \ --data-urlencode "sort=-rating_imdb" \ --data-urlencode "limit=3" \ --data-urlencode "fields=title,year,rating_imdb" \ "https://api.kinodata.space/v1.3/titles" ``` ### A director’s films ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "person=kp:41477" \ --data-urlencode "person.role=director" \ --data-urlencode "released=true" \ --data-urlencode "sort=-year" \ --data-urlencode "limit=3" \ --data-urlencode "fields=title,year" \ "https://api.kinodata.space/v1.3/titles" ``` ### Cursor ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "sort=-rating_imdb" \ --data-urlencode "votes_imdb.gte=100000" \ --data-urlencode "limit=2" \ --data-urlencode "fields=title,rating_imdb" \ --data-urlencode "cursor=" \ "https://api.kinodata.space/v1.3/titles" ``` ### Unknown filter ```bash curl -G -H "X-API-Key: $KINODATA_KEY" \ --data-urlencode "status=released" \ "https://api.kinodata.space/v1.3/titles" ``` --- # Title card > Everything about a movie or series in one request: titles, overviews, ratings, dates, awards, media and cast. > https://kinodata.space/en/docs/titles/card.md Request: `GET https://api.kinodata.space/v1.3/titles/{type}/{id}` The card opens by an ID from any of the five catalogs: `/titles/kp/447301` and `/titles/imdb/tt1375666` return the same thing. The full card has up to 22 blocks and is about 26 KB, about 8 KB compressed. If you need part of it, list the blocks in `fields`: the response gets smaller and easier to parse. The `ids` block always comes back. ## Path parameters - `type` (string, required) — The catalog the ID comes from: IMDb, Kinopoisk, TMDB (movie or series) or Kinorium. Values: `imdb`, `kp`, `tmdb`, `tmdb_tv`, `kinorium`. - `id` (string, required) — The ID in that catalog. IMDb IDs are `tt` followed by digits (`tt1375666`); the others are digits (`447301`). ## Query parameters - `fields` (csv) — Which blocks to return: `fields=title,ratings`. With a `-` sign, which to drop: `fields=-akas,-tags`. The two can’t be mixed. All blocks by default. Values: `ids`, `meta`, `title`, `type`, `synopsis`, `release`, `ratings`, `finance`, `awards`, `countries`, `classification`, `series`, `media`, `genres`, `companies`, `akas`, `tags`, `watch`, `parental`, `credits_top`, `franchise`, `counts`. Example: `fields=title,ratings`. - `lang` (ru | en) — Keep one language in the response. Without it, text fields come in `_ru` and `_en` pairs. More in [Response languages](https://kinodata.space/en/docs/languages.md). ## Response fields - `ids` (object) — The title's IDs in every catalog and links to its pages. Always returned, even if it isn't in `fields`. - `imdb_id` (string | null) — IMDb ID. - `kp_id` (string | null) — Kinopoisk ID. - `tmdb_id` (string | null) — TMDB movie ID. - `tmdb_tv_id` (string | null) — TMDB series ID. At most one of `tmdb_id` and `tmdb_tv_id` is set. - `kinorium_id` (string | null) — Kinorium ID. - `urls` (object) — Links to the title’s pages: `imdb`, `kp`, `tmdb`, `kinorium`. With `lang=en` the Kinorium link points to `en.kinorium.com`. - `meta` (object) — Service information about the card. - `updated_at` (string) — When the title’s data was last updated, ISO 8601 in UTC. - `title` (object) — Titles. - `title_ru` (string | null) — Russian title. - `title_en` (string | null) — English title. Only a real one: the original title of a non-English film or a transliteration never goes here, and then it is `null`. - `title_original` (string | null) — Title in the original language. - `type` (object) — Kind and status of the title. - `kind` (enum) — Kind of title. Values: `movie`, `series`, `mini_series`, `tv_show`, `anime`, `cartoon`, `short`. - `is_series` (boolean) — Whether the title has episodes. - `status` (enum) — Release status. Every value is in `/enums`, set `statuses`. Values: `released`, `ended`, `returning_series`, `in_production`, `post_production`, `in_development`, `planned`, `canceled`. - `kind_label_ru`, `kind_label_en` (string) — Kind label: «Фильм» / «Movie». - `status_label_ru`, `status_label_en` (string) — Status label: «Вышел» / «Released». - `synopsis` (object) — Overviews and taglines. Each text comes from its own source rather than being a translation of the other. - `overview_ru`, `overview_en` (string | null) — Plot overview. - `short_overview_ru`, `short_overview_en` (string | null) — A one- or two-sentence overview. - `tagline_ru`, `tagline_en` (string | null) — Tagline. - `overview_alter_ru`, `overview_alter_en` (string | null) — A second overview from another source, when it differs from the first. - `release` (object) — Year, runtime and release dates. - `year` (integer | null) — Release year. - `runtime` (integer | null) — Runtime in minutes. - `releases` (array) — Release dates by region. Each date appears once: if the US digital release matches the worldwide one, it is only under `world`. - `region` (string) — Region: `world` is the earliest date worldwide, `us` is the US, `ru` is Russia. - `dates` (array) — Dates in the region: `type` and `date`. Types: `premiere`, `release` for the theatrical release, `digital`; labels are in `/enums`, set `title_release_types`. Dates are `YYYY-MM-DD`. - `ratings` (object) — Ratings by source. There is no overall score on purpose: sources have different scales and audiences. - `sources` (object) — Ratings by source: the key is the source (`imdb`, `kp`, `tmdb`, `kinorium`, `letterboxd`, `rt`, `metacritic`, `critics`, `cr`), the value has `value`, `votes`, `scale` (10 or 100), `kind: "critics"` for critics’ scores and the page `url`. Kinorium also has `distribution`: how many viewers gave each score from 1 to 10. Kinorium counts it separately, so its sum doesn’t match `votes`. Source labels are in `/enums`, set `rating_sources`. No source, no key. - `votes_total` (integer | null) — Kinopoisk, IMDb and TMDB votes combined. - `divergence_kp_imdb` (number | null) — How far the Kinopoisk and IMDb ratings are apart, as an absolute value. - `popularity` (number | null) — TMDB’s popularity index. It is not a vote count. - `finance` (object) — Money, in US dollars. - `budget` (integer | null) — Budget. - `box_office_us` (integer | null) — US box office. - `revenue_world` (integer | null) — Worldwide box office. - `awards` (object) — Awards: totals and a breakdown by ceremony. - `wins_imdb`, `nominations_imdb` (integer | null) — Wins and all nominations, including the won ones, according to IMDb, across every award worldwide. - `oscars_won`, `oscars_nominated` (integer | null) — Oscars: wins and all nominations, including the won ones. - `wins_ceremonies`, `nominations_ceremonies` (integer) — Wins and all nominations across the ceremonies in `ceremonies`. - `summary_kinorium_ru` (string | null) — Kinorium’s per-ceremony summary, in Russian. - `summary_imdb_en` (string | null) — IMDb’s summary: “Won 4 Oscars. 160 wins & 220 nominations total”. A different text, not a translation of Kinorium’s. - `ceremonies` (array) — Ceremonies by prestige, Oscars first. - `slug` (string | null) — Ceremony code: `oscar`, `bafta`, `golden_globe`… - `ceremony_ru`, `ceremony_en` (string) — Ceremony name. - `icon` (string | null) — Path to the ceremony icon on `api.kinodata.space`: gold for a win, grey for a nomination. - `won` (boolean) — Whether there is at least one win. - `wins`, `nominations` (integer) — Wins and all nominations, including the won ones. - `years` (array) — Ceremony years. - `items` (array) — Categories: `category_ru`, `category_en`, `won`, `year`. - `countries` (array) — Production countries: `code` (ISO 3166-1, lowercase), `name_ru`, `name_en`. - `classification` (object) — Age rating and language. - `age_limit` (integer | null) — Russian age rating: 0, 6, 12, 16 or 18. - `mpaa` (string | null) — US certificate: `G`, `PG`, `PG-13`, `R`, `NC-17` or `TV-*`. - `original_language` (string | null) — Original language, ISO 639-1. - `is_adult` (boolean) — Adult content. - `series` (object) — Series data. All `null` for a movie. - `seasons_total`, `episodes_total` (integer | null) — Seasons and episodes. - `start_year`, `end_year` (integer | null) — Years on air. `end_year` is `null` for a running series. - `media` (object) — Posters, backdrop, trailers and soundtrack. Other stills and videos are in `/titles/{type}/{id}/media`. - `poster_url_ru`, `poster_url_en` (string | null) — Poster for a Russian and an English storefront. The English one has English lettering; `null` if there is none. - `poster_source_ru`, `poster_source_en` (string | null) — Where the poster comes from: `kp`, `tmdb`, `imdb`, `kinorium`. - `posters_by_source` (object) — Each source’s poster: `kp`, `tmdb`, `imdb`, `kinorium`. - `backdrop_url` (string | null) — A wide still for a background. - `poster_animated` (string) — Kinorium’s animated poster: an mp4 clip of up to 7 MB. About one popular title in ten has it; when it’s missing, so is the key. Show it in `