IMDb Title Rating Lookup

Site imdb.comTask get-rating-16ss93Version v1Updated Jul 13, 2026Category entertainment

Resolve any IMDb title URL / tt-ID / free-form title reference (movie, TV series, episode, mini-series, short) to its current IMDb rating, total vote count, rating distribution per 1-10 bucket, Metascore, and core title metadata (cast, directors, writers, genres, runtime, certification, plot, languages, countries, poster, canonical URL). Read-only. This skill was captured from a live agent session on imdb.com and publishes here verbatim, exactly as an agent receives it.

NoteSelectors and URL schemes drift as sites change. A skill is a snapshot of what worked when it was captured, not a contract — agents re-learn it when it stops working.

Purpose

Given an IMDb title URL, IMDb title ID (tt...), or free-form title reference (movie / TV show / TV episode / mini-series / short / documentary), return the current IMDb rating, total vote count, rating distribution (votes per 1-10 bucket when shown), and the core title metadata: primary title, original title (when different), title type (movie / tvSeries / tvEpisode / tvMiniSeries / short / documentary / videoGame), release year (or year range for series), MPAA / TV certification, runtime in minutes, genres, Metascore (when present), top-billed cast with role names, directors, writers, primary poster URL, short + long plot summary, language(s), country/countries of origin, and the canonical IMDb URL. For TV episodes additionally return parent series ID + title and season/episode numbers. Read-only — never click Rate, Add to Watchlist, Sign In, or any mutation control.

When to Use

  • "What's the IMDb rating of {movie/show}?"
  • Bulk enrichment of a watchlist / spreadsheet of titles — pass a free-form name or a known tt-ID per row.
  • Comparing the user-rating + Metascore + distribution shape across a candidate set.
  • Resolving an ambiguous free-form title to a canonical tt-ID before scraping any other IMDb subpage.
  • Pulling the JSON-LD aggregateRating for any IMDb title type, including TV episodes (/title/tt.../episodes/).

Workflow

The optimal flow is two-staged:

  1. Resolve free-form input → tt-ID via IMDb's public-but-undocumented suggestion API (no auth, no anti-bot, no proxy). This is the same JSON the IMDb search-bar typeahead uses. Always use this first unless the caller already passed a tt-ID or a /title/tt.../ URL.
  2. Fetch the canonical title page https://www.imdb.com/title/{ttId}/ and extract from its static HTML — primarily the <script type="application/ld+json"> block and the <script id="__NEXT_DATA__" type="application/json"> blob. The title page is protected by AWS WAF (AwsWafIntegration token challenge) which returns a 202 with a ~2 KB JS-challenge body to any raw HTTP client (including a browserless_function in-page fetch, even through a residential proxy). Drive it with browserless_agent and proxy: { proxy: "residential" }. The WAF clears automatically when JS executes in the real browser.

1. Resolve free-form input → tt-ID (skip if you already have the ID)

The suggestion API is rooted at https://v3.sg.media-imdb.com/suggestion/{firstChar}/{slug}.json. The {firstChar} path component is ignored server-side — any of h, t, or the actual first character of {slug} returns the same response. Build {slug} from the user query by replacing spaces with _ and lowercasing:

SLUG=$(echo "$query" | tr '[:upper:]' '[:lower:]' | tr ' ' '_' | sed 's/[^a-z0-9_]//g')
# v3.sg.media-imdb.com is WAF-exempt JSON — a plain HTTPS GET from any client works:
GET "https://v3.sg.media-imdb.com/suggestion/t/${SLUG}.json"

The suggestion host is WAF-exempt, so this is a plain HTTPS GET (any client, no proxy, no browser). Under restricted egress, route it via browserless_function that page.goto('https://v3.sg.media-imdb.com/') then page.evaluates a same-origin fetch of the /suggestion/...json path.

Response shape — d[] is an ordered list of matches:

{"d":[
  {"id":"tt0111161","l":"The Shawshank Redemption","q":"feature","qid":"movie",
   "rank":78,"s":"Tim Robbins, Morgan Freeman","y":1994,
   "i":{"imageUrl":"https://m.media-amazon.com/...","height":1800,"width":1200}},
  ...
]}

Key fields:

  • id — the tt-prefixed title ID. This is your handoff to step 2.
  • l — title.
  • q — human-readable type ("feature", "TV series", "TV mini-series", "TV episode", "TV short", "short", "TV movie", "video", "podcastSeries", "videoGame").
  • qid — machine type (movie, tvSeries, tvMiniSeries, tvEpisode, tvShort, short, tvMovie, video, podcastSeries, videoGame).
  • y — year (a single integer). Series additionally carry yr as a "YYYY-YYYY" range string (open-ended ongoing series have "YYYY-").
  • rankIMDb popularity rank (lower = more popular). Do not confuse with the user ratingrank is MOVIEmeter-style popularity, NOT the 0.0-10.0 user score. The user rating is not exposed via the suggestion API at all.
  • s — short top-cast string (comma-separated names, no roles).
  • i — poster image URL + native dimensions.

Disambiguation heuristics (run in order until a single best match is left):

  1. If the input includes a 4-digit year (e.g. "the matrix 1999"), filter d[] to entries where y === year.
  2. If the input includes a type hint ("TV", "series", "movie", "episode", "documentary"), filter d[] by matching qid.
  3. If multiple candidates remain, pick the lowest rank (most popular). If rank is missing on a candidate, treat as Infinity.
  4. If the top two candidates have very close rank values (within 10× of each other) and the query is ambiguous, emit a success: false, reason: "ambiguous_name" result with the top 3-5 candidates rather than guessing.

For TV episodes: the suggestion API surfaces well-known episodes (e.g. "breaking bad ozymandias"tt2301451) but tends to under-rank lesser-known episode pages. If the query says "season N finale" / "S5E14" / etc. and the suggestion API returns the parent series instead of the episode, fall back to resolving the series first, then navigating to /title/{seriesId}/episodes/?season={N} and reading the episode-list page (or jumping to /title/{episodeId}/).

2. Fetch the title page and extract the rating + metadata

Drive the title page in one browserless_agent call with proxy: { proxy: "residential" }. Batch the whole nav → settle → extract flow inside that single call's commands array — it saves round-trips and avoids accidentally dropping the proxy between calls (there is no separate session-release step):

{
  "proxy": { "proxy": "residential" },
  "commands": [
    {
      "method": "goto",
      "params": {
        "url": "https://www.imdb.com/title/${ttId}/",
        "waitUntil": "load",
        "timeout": 45000
      }
    },
    { "method": "waitForTimeout", "params": { "time": 1500 } },
    {
      "method": "evaluate",
      "params": {
        "content": "(()=>{const ld=document.querySelector('script[type=\"application/ld+json\"]')?.textContent||null;const nd=document.getElementById('__NEXT_DATA__')?.textContent||null;return JSON.stringify({ld,nd});})()"
      }
    }
  ]
}
  • waitUntil: "load" on goto (never networkidle — it hangs on IMDb's SPA); the waitForTimeout 1500 lets lazy hydration settle.
  • Extracting the two <script> payloads with an in-page evaluate (above) is preferred over shipping raw HTML — it stays well under the result-size cap. If you'd rather parse HTML host-side, use { "method": "html", "params": { "selector": "body" } } instead and run the regexes below on the returned string.
  • A residential proxy is required — without it the AWS WAF challenge stalls (a raw HTTP fetch just gets the 202 challenge) and IMDb IP-rate-limits a bare session within a few requests. Driving the page in a real browser (browserless_agent) with a residential proxy clears the WAF automatically (JS executes) and renders the title page normally (no captcha, no login wall).

2a. Extract <script type="application/ld+json">

The first application/ld+json block on every IMDb title page is a schema.org Movie / TVSeries / TVEpisode object that contains everything you need for the headline rating + most metadata:

{
  "@context": "https://schema.org",
  "@type": "Movie",
  "url": "https://www.imdb.com/title/tt0111161/",
  "name": "The Shawshank Redemption",
  "alternateName": "Cadena perpetua",
  "image": "https://m.media-amazon.com/images/M/MV5B...jpg",
  "datePublished": "1994-10-14",
  "contentRating": "R",
  "duration": "PT2H22M",
  "genre": ["Drama"],
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": 9.3,
    "ratingCount": 3050000,
    "bestRating": 10,
    "worstRating": 1
  },
  "actor":   [{"@type":"Person","url":"...","name":"Tim Robbins"}, ...],
  "director":[{"@type":"Person","url":"...","name":"Frank Darabont"}],
  "creator": [{"@type":"Organization","url":"..."}, {"@type":"Person","url":"...","name":"Stephen King"}],
  "description": "Over the course of several years, two convicts form a friendship..."
}

Parse it with a hardened regex (NOT JSON.parse on raw HTML; the block may contain HTML-entity-escaped characters in description):

const m = html.match(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/);
const ld = JSON.parse(m[1]);

Field mapping (ld → output JSON):

Output fieldLD-JSON source
titleIdparse from ld.url (/title/(tt\d+)/)
titleld.name
originalTitleld.alternateName if present and !== ld.name, else null
titleTypederive from ld["@type"] (Moviemovie, TVSeriestvSeries, TVEpisodetvEpisode, TVMiniSeriestvMiniSeries, Shortshort, VideoGamevideoGame); fall back to __NEXT_DATA__ (see 2b) when @type is generic.
yearyear part of ld.datePublished (or __NEXT_DATA__.releaseYear.year for safety).
yearRangeseries only — from __NEXT_DATA__ (2b).
certificationld.contentRating
runtimeMinutesparse ISO-8601 ld.duration (PT2H22M → 142). Some shorts use PT15M; some series use PT45M as per-episode runtime.
genresld.genre (string → wrap in array)
imdbRatingld.aggregateRating.ratingValue
voteCountld.aggregateRating.ratingCount
actorsld.actor[].name (typically top 5; IMDb truncates here — for the full top-billed list use __NEXT_DATA__, see 2b)
directorsld.director[].name (object or array — normalize to array)
writersld.creator[] filtered to @type === "Person"
posterUrlld.image
shortPlotld.description (HTML-entity-decode after parse)
canonicalUrlld.url

aggregateRating may be absent when a title has fewer than 5 user votes (unrated). Handle missing-gracefully: emit imdbRating: null, voteCount: 0 rather than throwing.

2b. Extract <script id="__NEXT_DATA__" type="application/json">

The LD-JSON block is insufficient for some required fields:

  • Rating distribution per 1-10 bucket (not in LD-JSON at all).
  • Metascore (not in LD-JSON).
  • Full cast list (LD-JSON truncates at ~5).
  • Languages (spokenLanguages).
  • Countries of origin (countriesOfOrigin).
  • TV-episode parent-series ID + season/episode numbers.
  • TV-series year range (endYear).

All of these live in the Next.js page-data blob:

const nm = html.match(
  /<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
);
const nd = JSON.parse(nm[1]);
const title = nd.props.pageProps.mainColumnData; // root for most title fields
const above = nd.props.pageProps.aboveTheFoldData; // root for rating + summary

Useful paths inside mainColumnData / aboveTheFoldData (paths stable across iters; field names match IMDb's internal GraphQL schema):

  • aboveTheFoldData.ratingsSummary.aggregateRating — same ratingValue (decimal).
  • aboveTheFoldData.ratingsSummary.voteCount — same total as LD-JSON.
  • mainColumnData.ratingsSummary.histogram.histogramValuesrating distribution, an array of 10 objects {rating: 10, voteCount: N} from rating 10 down to rating 1. The order is descending — always sort or map by rating rather than relying on positional index.
  • aboveTheFoldData.metacritic.metascore.score — Metascore (or null when no Metascore).
  • mainColumnData.cast.edges[] — full cast; each edge has node.name.nameText.text (actor name), node.characters[].name (role names), node.attributes[].text ("voice", "uncredited", etc.).
  • mainColumnData.principalCredits[] — director/writer/creator grouped by role (category.id === "director" | "writer" | "creator").
  • mainColumnData.spokenLanguages.spokenLanguages[].text — languages.
  • mainColumnData.countriesOfOrigin.countries[].text — countries.
  • mainColumnData.plot.plotText.plainText — short plot (same as LD-JSON description).
  • mainColumnData.outline.plotText.plainText — outline (often null).
  • For series: mainColumnData.releaseYear.year + mainColumnData.releaseYear.endYear (endYear null for ongoing series).
  • For episodes: mainColumnData.series.series.id (parent series tt-ID), mainColumnData.series.series.titleText.text (parent series title), mainColumnData.series.episodeNumber.seasonNumber, mainColumnData.series.episodeNumber.episodeNumber.

For the FULL plot summary (the multi-paragraph "Storyline" block), the __NEXT_DATA__ blob carries it at mainColumnData.summaries.edges[0].node.plotText.plaintext (or null if only a synopsis exists). When you need a longer plot than ld.description, prefer this path.

3. Session lifecycle

No session-release step is needed — there is nothing to release. The session is not torn down when the call returns, though: it persists across calls, keyed by the proxy config (repeat the same proxy to reconnect to the same warmed, WAF-cleared session; dropping or changing it lands you in a different, blank session). Batching the goto → settle → extract sequence inside a single call's commands array saves round-trips and avoids accidentally dropping that config.

Browser fallback (no API shortcut needed for rating data)

There is no public API surface that returns the IMDb user-rating value. The suggestion API in step 1 is purely a name-resolver. The title-page HTML is the only path to the rating + distribution + Metascore. Don't waste cycles chasing caching.graphql.imdb.com or api.graphql.imdb.com — verified blocked / 500 to anonymous clients (see Site-Specific Gotchas).

Site-Specific Gotchas

  • AWS WAF (AwsWafIntegration) on every www.imdb.com/title/* HTML request from non-browser clients. A raw HTTP fetch — including a browserless_function in-page fetch, with or without a residential proxy — returns HTTP 202 and a ~2 KB body containing an awswaf.com/challenge.js token-acquisition handshake, not the real page. The challenge clears only when JS executes, so the title page must be driven with browserless_agent (a real browser session). Verified across https://www.imdb.com/title/tt0111161/, /title/.../episodes/, /find/, /_next/data/..., /sitemap.xml, /_json/... — every WAF-protected path returns the same 1991-byte challenge.
  • A raw HTTP fetch is NOT a viable surface for IMDb title pages. A plain HTTPS GET (including browserless_function's in-page fetch) gets the 202 WAF challenge; you must drive the page with browserless_agent. Raw fetch is fine only for the suggestion API (v3.sg.media-imdb.com) and robots.txt — both are WAF-exempt. All www.imdb.com paths the future agent cares about are WAF-protected.
  • Use a residential proxy on the browserless_agent call. Set proxy: { proxy: "residential" }. Bare sessions get WAF-challenged or IP-rate-limited after a handful of requests. Driving the page in the real browser clears the challenge automatically (JS executes); the residential proxy rotates the source IP to avoid the rate-limit ban that triggers around request 10-20 from the same datacenter IP.
  • IMDb's robots.txt blocks AI crawlers. Lines User-agent: anthropic-ai / Claude-Web / GPTBot / CCbot / Google-Extended → Disallow: / are present in https://www.imdb.com/robots.txt. The skill must drive a real browser (with a non-bot UA), not curl-fetch with an AI-bot UA. browserless_agent uses a real Chrome UA and clears this.
  • Suggestion-API {firstChar} path component is decorative. https://v3.sg.media-imdb.com/suggestion/h/the_matrix.json and /suggestion/t/the_matrix.json and /suggestion/0/the_matrix.json all return identical JSON. The IMDb search-bar typeahead conventionally sends the first character of the query; the server doesn't care.
  • Suggestion-API rank is MOVIEmeter popularity, NOT user rating. A common trap. The user rating (aggregateRating.ratingValue) is not in the suggestion JSON at all — only the title-page HTML carries it.
  • aggregateRating is missing from the LD-JSON block when a title has fewer than ~5 user votes (typical for obscure shorts, unreleased titles, video-game expansions). Treat as imdbRating: null, voteCount: 0 rather than failing.
  • Rating distribution lives ONLY in __NEXT_DATA__, not in the LD-JSON block. The path is mainColumnData.ratingsSummary.histogram.histogramValues and the array is sorted descending by rating (10 → 1). Always map by rating field; do not assume index 0 == 10.
  • LD-JSON actor array is truncated (typically 5 entries). For the full top-billed cast, parse __NEXT_DATA__.props.pageProps.mainColumnData.cast.edges[].
  • Runtime in LD-JSON is ISO-8601, not minutes. PT2H22M → 142, PT45M → 45. For series, this is the per-episode runtime, not total — note that in the output if the title type is tvSeries/tvMiniSeries.
  • datePublished for series is the series premiere date, not the year range. For a yearRange field on series, read __NEXT_DATA__.mainColumnData.releaseYear.year (start) and .endYear (null for ongoing).
  • Episode pages are also title pages. A TV-episode tt-ID has its own /title/tt.../ page with the same LD-JSON + __NEXT_DATA__ structure. To get parent-series context, read mainColumnData.series.series.id / .titleText.text and mainColumnData.series.episodeNumber.seasonNumber / .episodeNumber.
  • IMDbPro is a different surface (pro.imdb.com). It loads without the WAF challenge but exposes MOVIEmeter / production-contact data, not the public user-rating. Don't use it for rating lookup.
  • IMDb GraphQL is a trap for anonymous clients. Both caching.graphql.imdb.com and api.graphql.imdb.com return 301 → 500 (or block) without a session-cookied request from a logged-in page context. Don't try to bypass the title-page HTML this way.
  • Bulk-data alternative for offline use. IMDb publishes daily TSVs at https://datasets.imdbws.com/ (title.basics.tsv.gz, title.ratings.tsv.gz — only rating + numVotes, no distribution). Useful for batch enrichment of millions of tt-IDs; not appropriate for "what's the rating right now" lookups (24-hour staleness) or for distribution / Metascore (not in the dataset).
  • Read-only — never click Rate, Add to Watchlist, Sign in, or any star-rating bucket. Those mutate user state and require an authenticated user.
  • Original-title detection. ld.alternateName is the original-language title for foreign-language films (e.g. "Cadena perpetua" for tt0111161's Spanish release). It is also populated for some English-language films with regional retitles, so compare alternateName !== name before treating it as "original title".
  • Free-form queries with city/country names don't get rerouted the way OpenTable's term-parser reroutes them — the IMDb suggestion API is purely textual. Safe to pass "Joe's Shanghai" as a movie title without disambiguation tricks.
  • The rating data requires a real browser session. The suggestion API (step 1) is a plain WAF-exempt HTTPS GET that works from any client, but the title page — the only source of the rating/distribution/Metascore — is WAF-walled and must be driven with browserless_agent. An environment that can only make plain HTTP requests can resolve the tt-ID but cannot get the rating; make sure browserless_agent is available before running.

Expected Output

Single, consistent shape — variants by title type are reflected in titleType and the optional seriesContext block.

Movie

{
  "success": true,
  "titleId": "tt0111161",
  "title": "The Shawshank Redemption",
  "originalTitle": null,
  "titleType": "movie",
  "year": 1994,
  "yearRange": null,
  "certification": "R",
  "runtimeMinutes": 142,
  "genres": ["Drama"],
  "imdbRating": 9.3,
  "voteCount": 3050000,
  "ratingDistribution": [
    { "rating": 10, "voteCount": 1830000 },
    { "rating": 9, "voteCount": 580000 },
    { "rating": 8, "voteCount": 320000 },
    { "rating": 7, "voteCount": 150000 },
    { "rating": 6, "voteCount": 70000 },
    { "rating": 5, "voteCount": 38000 },
    { "rating": 4, "voteCount": 18000 },
    { "rating": 3, "voteCount": 12000 },
    { "rating": 2, "voteCount": 8000 },
    { "rating": 1, "voteCount": 25000 }
  ],
  "metascore": 82,
  "cast": [
    { "name": "Tim Robbins", "role": "Andy Dufresne" },
    { "name": "Morgan Freeman", "role": "Ellis Boyd 'Red' Redding" },
    { "name": "Bob Gunton", "role": "Warden Norton" },
    { "name": "William Sadler", "role": "Heywood" },
    { "name": "Clancy Brown", "role": "Captain Hadley" }
  ],
  "directors": ["Frank Darabont"],
  "writers": ["Stephen King", "Frank Darabont"],
  "posterUrl": "https://m.media-amazon.com/images/M/MV5BMDAyY2FhYjctNDc5OS00MDNlLThiMGUtY2UxYWVkNGY2ZjljXkEyXkFqcGc@._V1_.jpg",
  "shortPlot": "Over the course of several years, two convicts form a friendship, seeking consolation and, eventually, redemption through basic compassion.",
  "fullPlot": "Chronicles the experiences of a formerly successful banker as a prisoner...",
  "languages": ["English"],
  "countries": ["United States"],
  "canonicalUrl": "https://www.imdb.com/title/tt0111161/",
  "seriesContext": null
}

TV Series

{
  "success": true,
  "titleId": "tt11280740",
  "title": "Severance",
  "originalTitle": null,
  "titleType": "tvSeries",
  "year": 2022,
  "yearRange": "2022-",
  "certification": "TV-MA",
  "runtimeMinutes": 60,
  "genres": ["Drama", "Mystery", "Sci-Fi", "Thriller"],
  "imdbRating": 8.7,
  "voteCount": 450000,
  "ratingDistribution": [ {"rating": 10, "voteCount": 0}, ... ],
  "metascore": 87,
  "cast": [ {"name": "Adam Scott", "role": "Mark Scout"}, ... ],
  "directors": [],
  "writers": ["Dan Erickson"],
  "posterUrl": "https://...",
  "shortPlot": "...",
  "fullPlot": "...",
  "languages": ["English"],
  "countries": ["United States"],
  "canonicalUrl": "https://www.imdb.com/title/tt11280740/",
  "seriesContext": null
}

TV Episode

{
  "success": true,
  "titleId": "tt2301451",
  "title": "Ozymandias",
  "originalTitle": null,
  "titleType": "tvEpisode",
  "year": 2013,
  "yearRange": null,
  "certification": "TV-MA",
  "runtimeMinutes": 48,
  "genres": ["Crime", "Drama", "Thriller"],
  "imdbRating": 10.0,
  "voteCount": 250000,
  "ratingDistribution": [ ... ],
  "metascore": null,
  "cast": [ ... ],
  "directors": ["Rian Johnson"],
  "writers": ["Vince Gilligan", "Moira Walley-Beckett"],
  "posterUrl": "https://...",
  "shortPlot": "...",
  "fullPlot": "...",
  "languages": ["English"],
  "countries": ["United States"],
  "canonicalUrl": "https://www.imdb.com/title/tt2301451/",
  "seriesContext": {
    "seriesId": "tt0903747",
    "seriesTitle": "Breaking Bad",
    "seasonNumber": 5,
    "episodeNumber": 14
  }
}

Failure shapes

// Unrated (fewer than ~5 user votes — aggregateRating missing from LD-JSON)
{
  "success": true,
  "titleId": "tt99999999",
  "title": "Some Obscure Short",
  "titleType": "short",
  "imdbRating": null,
  "voteCount": 0,
  "ratingDistribution": [],
  "metascore": null,
  ...
}

// Free-form input could not be confidently resolved to a single tt-ID
{
  "success": false,
  "reason": "ambiguous_name",
  "query": "severance",
  "candidates": [
    {"titleId": "tt11280740", "title": "Severance", "year": 2022, "titleType": "tvSeries", "rank": 150},
    {"titleId": "tt0464196",  "title": "Severance", "year": 2006, "titleType": "movie",    "rank": 8508}
  ]
}

// Free-form input returned zero matches from the suggestion API
{
  "success": false,
  "reason": "title_not_found",
  "query": "ksjdhfksjdhfksjdhf"
}

// WAF challenge could not be cleared (rare with a residential-proxy browserless_agent session; document and retry on a fresh session)
{
  "success": false,
  "reason": "anti_bot_block",
  "titleId": "tt0111161",
  "detail": "AWS WAF AwsWafIntegration challenge did not clear after 3 attempts"
}

Call it

GET https://production-sfo.browserless.io/skills?token=TOKEN-HERE&domain=imdb.com&task=get-rating-16ss93