Rotten Tomatoes Title Rating
Purpose
Given a Rotten Tomatoes title URL, RT slug (m/<slug> or tv/<slug> / tv/<slug>/s<N>), or free-form title reference ("The Matrix", "Severance season 2"), return current Tomatometer (critic) score, Popcornmeter / Audience score, certified flags, vote counts, sample critic reviews, full cast & crew, synopsis, where-to-watch affiliates, and core title metadata as one structured JSON object. Read-only — never clicks "Want to See", "Rate", "Sign In", or any audience-rating control.
When to Use
- Spot-checking a movie / TV show's current Tomatometer and Popcornmeter (the canonical "should I watch this?" lookup).
- Bulk extraction across a list of titles for a recommendation engine, watchlist enricher, or release-monitoring agent.
- Resolving a free-form title string ("Severance season 2", "the matrix 1999") to a canonical RT page + score.
- Comparing critics vs audience sentiment for the same title.
- Pulling per-season Tomatometer + Popcornmeter for TV (the series page only surfaces a series-wide average; per-season scores live on the season URL).
Workflow
Rotten Tomatoes is server-rendered HTML — the score, cast, synopsis, where-to-watch, and even the top critic-review cards are all in the initial HTML response. Two embedded JSON blobs do the heavy lifting:
<script ... id="media-scorecard-json" ... type="application/json">— Tomatometer + Popcornmeter, sentiment, certified flags, rating counts, average rating, banded count, and theoverlay.audienceVerified/overlay.criticsTopsubsets when present.<script type="application/ld+json">— schema.orgMovie/TVSeries/TVSeasonwithaggregateRating,actor[],director[],producer[],genre[],contentRating(MPAA / TV rating),dateCreated(release date),containsSeason[](for TV series),partOfSeries(for TV season),numberOfSeasons,image(poster), and canonicalurl.
A bb fetch against the canonical URL returns 200 with all of this — no browser, no proxy, no Verified required. There is no public JSON API; this skill is "static-HTML-as-API." Lead with the fetch path; the browser path is a fallback if Akamai ever starts blocking (currently never observed during converged iters from the Browserbase Fetch API).
1. Resolve the URL
Three input shapes feed one canonical URL:
(a) Full Rotten Tomatoes URL — use as-is, but normalize: RT 301-redirects some legacy slugs (e.g. /m/the_matrix → /m/matrix). Follow the redirect (bb fetch does not auto-follow; check statusCode === 301 and re-fetch the Location header).
(b) Slug (m/the_matrix, tv/severance, tv/severance/s02) — prepend https://www.rottentomatoes.com/ and treat as case (a).
(c) Free-form title — hit the search results page:
GET https://www.rottentomatoes.com/search?search=<URL-encoded query>
The search page is also server-rendered. Each match is a <search-page-media-row> web component with attributes carrying the disambiguation signals:
release-year="1999"
start-year="" end-year="" (TV: "2022" "2026")
cast="Keanu Reeves,Laurence Fishburne,Carrie-Anne Moss"
tomatometer-score="83"
tomatometer-is-certified="true"
tomatometer-sentiment="POSITIVE"
…with <a href="https://www.rottentomatoes.com/m/matrix" data-qa="thumbnail-link"> carrying the canonical URL. Sections are split by <search-page-result type="movie|tvSeries">. The page also surfaces filter counts: Movies (68) | TV Shows (11704). For a query like "Severance season 2", pick the TV-show result then construct the season URL /tv/<slug>/sNN (or take the slug from the containsSeason[] array on the series page).
If the query has a year token, prefer the row whose release-year matches. If a season N is in the query and the matched TV row has multiple seasons, fetch the series page first to read containsSeason[].url and pick the right /sNN URL.
2. Fetch the title page
bb fetch "https://www.rottentomatoes.com/m/matrix"
# or "https://www.rottentomatoes.com/tv/severance"
# or "https://www.rottentomatoes.com/tv/severance/s02"
Returns { statusCode: 200, content: "<full HTML>" }. --proxies is NOT required — direct bb fetch succeeds from api.browserbase.com's Fetch API. Add --proxies only if a 403 / "Access Denied" surfaces (rare; Akamai is configured permissively for these pages).
3. Extract the scores
Match the scorecard JSON (note the multi-line attribute layout — [\s\S]*? between <script and id="..."):
const re = /<script[\s\S]*?id="media-scorecard-json"[\s\S]*?>([\s\S]*?)<\/script>/;
const j = JSON.parse(html.match(re)[1]);
j.criticsScore:
score(string"83") → Tomatometer 0–100sentiment∈"POSITIVE" | "NEGATIVE"(missing for no-score)certified: true→Certified FreshstatusratingCount→ number of critic reviewsaverageRating(string"7.90") → average critic numerical rating out of 10likedCount,notLikedCount→ fresh-vs-rotten review tally
j.audienceScore:
score(string"85") → Popcornmeter 0–100sentiment∈"POSITIVE" | "NEGATIVE"(missing for no-score)certified: true→ audience-side "Verified Hot" tier (also signalled byj.audienceScore.certifiedFresh === "certified")scoreType∈"ALL" | "VERIFIED"— which subset is the primary surface on this pagereviewCount→ exact audience rating countbandedRatingCount(string"250,000+ Ratings"/"5,000+ Ratings") → display-friendly bucketaverageRating(string"3.6") → audience average out of 5likedCount,notLikedCount→ liked-vs-disliked tally
j.overlay (the score-details popup payload — usually richer than the primary surface):
criticsAll/criticsTop— full critic pool vs top-critics-only subsetaudienceAll/audienceVerified— full audience pool vs verified-purchasers subset (RT splits these; the primaryaudienceScoreblock mirrors whichever subset the page chose to highlight)mediaType∈"Movie" | "TvSeries" | "TvSeason"— definitive title-type signal
Status string derivation (RT uses these labels publicly; emit them in your output):
| Tomatometer (critic) | Condition |
|---|---|
Certified Fresh | criticsScore.certified === true |
Fresh | criticsScore.sentiment === "POSITIVE" and not certified |
Rotten | criticsScore.sentiment === "NEGATIVE" |
No score yet | criticsScore.score is undefined / ratingCount === 0 |
| Popcornmeter (audience) | Condition |
|---|---|
Verified Hot | audienceScore.certified === true (or audienceScore.certifiedFresh === "certified") |
Upright | audienceScore.sentiment === "POSITIVE" and not certified |
Spilled | audienceScore.sentiment === "NEGATIVE" |
No score yet | audienceScore.score is undefined |
4. Extract title metadata (JSON-LD)
Match the JSON-LD block:
const ldRe = /<script[^>]+application\/ld\+json[^>]*>([\s\S]*?)<\/script>/g;
Keys used:
@type∈"Movie" | "TVSeries" | "TVSeason"— title type. (Note:j.overlay.mediaTypein the scorecard says"Movie" | "TvSeries" | "TvSeason"— same info, different casing.)name— primary titlecontentRating— MPAA / TV rating ("R","PG-13","TV-MA", …)dateCreated— release date ("1999-03-31"); series shows the original series start, season shows that season's premieregenre[]— list of stringsactor[]— top-billed cast as{name, sameAs (RT celebrity URL), image}. Character/role names are NOT in JSON-LD — pull them from the cast section HTML (step 6).director[],producer[]— names + RT URLsimage— poster URL (full-res Flixster CDN)description— RT's SEO blurb; replace withj.descriptionfrom the scorecard JSON for the proper synopsis bodynumberOfSeasons— TV series onlycontainsSeason[]— TV series only;[{@type:"TVSeason", name:"Season 1", url:"https://www.rottentomatoes.com/tv/severance/s01"}, …]. Use this to enumerate season URLs.partOfSeries— TV season only;{@type:"TVSeries", name:"Severance", startDate:"2022-02-18", url:"…/tv/severance"}. Use this to backlink the season to its parent.
5. Extract media-info (runtime / distributor / production / release / box office)
These live in the "Media Info" section as <dt> / <dd> pairs. Each item is wrapped as:
<div class="category-wrap" data-qa="item">
<dt class="key">… <rt-text … data-qa="item-label">Runtime</rt-text> </dt>
<dd data-qa="item-value-group">
<rt-text data-qa="item-value">2h 16m</rt-text>
</dd>
</div>
Pull label/value pairs with one regex sweep:
const itemRe = /<rt-text[^>]+data-qa="item-label">([^<]+)<\/rt-text>[\s\S]*?<dd[^>]+data-qa="item-value-group">([\s\S]*?)<\/dd>/g;
…then strip nested tags from each value. Known labels (case-stable): Runtime, Original Language, Release Date (Theaters), Release Date (Streaming), Rerelease Date (Theaters), Distributor, Production Co, Sound Mix, Aspect Ratio, Box Office (Gross USA), Most Popular at Home. For TV: Premiere Date, Network, Genre, Executive Producer, etc.
6. Extract cast tiles (with character/role names)
Cast tiles in data-qa="section:cast-and-crew" carry name + role inline:
<a href="/celebrity/keanu_reeves" data-qa="person-item">
…
<div slot="inset-text" aria-label="Keanu Reeves, Thomas "Neo" Anderson">
<p class="name" data-qa="person-name">Keanu Reeves</p>
<p class="role" data-qa="person-role">Thomas "Neo" Anderson</p>
</div>
</a>
Each tile is one of: a director, a writer, or a cast member with a character role. Pair data-qa="person-name" and data-qa="person-role" within the same data-qa="person-item" anchor.
7. Extract critic reviews
Top critic reviews are rendered as <review-card-critic> web components in the data-qa="section:critics-reviews" section. Each card has named slots:
<review-card-critic approved-critic approved-publication top-critic top-publication>
<rt-link slot="name" href="https://www.rottentomatoes.com/critic/joe-morgenstern"> Joe Morgenstern </rt-link>
<rt-text slot="publication"> Wall Street Journal </rt-text>
<span slot="timestamp">07/13/2023</span>
<div slot="rating">
<score-icon-critics sentiment="POSITIVE"></score-icon-critics>
<span>2.5/4</span>
</div>
<span slot="review">Though The Matrix ultimately overdoses on gloom-and-doom grunge…</span>
<rt-link slot="review-link" href="https://web.archive.org/web/…/lfilm598.htm">Go to Full Review</rt-link>
</review-card-critic>
The card-level attributes are signal flags: top-critic, top-publication, approved-critic, approved-publication. Use them to weight the sample. About 10 cards render per title page; the full list is at /m/<slug>/reviews (or /reviews/top-critics) if a larger sample is required.
The critics' consensus blurb is at id="critics-consensus" class="consensus" as a single <p>.
8. Extract "Where to Watch" (streaming-available-on)
const wtw = JSON.parse(html.match(/<script id="where-to-watch-json"[^>]*>([\s\S]*?)<\/script>/)[1]);
// wtw.affiliates: [{icon:"fandango-at-home", url:"…", isSponsoredLink:false, text:"Fandango at Home"}, …]
// wtw.affiliatesText: "Rent The Matrix on Fandango at Home, or buy it on Fandango at Home."
// wtw.hasShowtimes, wtw.showtimesUrl — populated only for currently-in-theaters titles
9. (Optional) Audience review samples
A second JSON script (no id, but contains "audienceScore" + "reviews":[…] at the top level — ~6th <script type="application/json"> on a typical page) holds the first ~5 audience reviews with displayName, displayDate, rating (out of 5), review body, and isVerified flag. Skip this unless explicitly requested — the task is critic-led.
Browser fallback
Browser-driving is not required for any observed page state. If bb fetch ever starts returning 403, recover with:
SID=$(bb sessions create --keep-alive --verified --proxies | <id-extract>)
browse --connect "$SID" open "https://www.rottentomatoes.com/m/<slug>"
browse --connect "$SID" wait load
HTML=$(browse --connect "$SID" get html body)
bb sessions update "$SID" --status REQUEST_RELEASE
The same JSON blobs render server-side into the live DOM — extract identically. Add --verified only if a bare proxied session still 403s.
Site-Specific Gotchas
- Slug redirects: Several canonical-feeling slugs 301 to a shorter form —
/m/the_matrix→/m/matrixis the textbook example.bb fetchdoes not auto-follow; detectstatusCode === 301and re-fetchheaders.Location. Always emit the final URL as the canonical one. - Multi-line script attributes break naïve regex: The
<script id="media-scorecard-json" …>tag is laid out across multiple lines on movie / TV-show pages (not minified). A regex like/<script id="media-scorecard-json"[^>]*>/fails on these pages because[^>]does not match the newline-prefixed attribute layout. Use/<script[\s\S]*?id="media-scorecard-json"[\s\S]*?>([\s\S]*?)<\/script>/. Verified onthe_odyssey_2026(pre-release) andavatar_fire_and_ash(released): single-line on some, multi-line on others, presumably depending on which Next.js page template hits. - TV series page shows averaged scores, not the latest season: On
/tv/<slug>the scorecardcriticsScore.titleis"Avg. Tomatometer"andaudienceScore.titleis"Avg. Popcornmeter"— these are aggregates across all seasons. For a specific season's score, fetch/tv/<slug>/sNNand read the scorecard there (itstitlewill be the unprefixed"Tomatometer"/"Popcornmeter"). When the user asks for "Severance ratings" without specifying a season, return both: the series average (withis_average: true) and the latest season (fromcontainsSeason[].url). audienceScoremirrors a chosen subset:audienceScore.scoreTypeswitches between"ALL"and"VERIFIED"per page. The page chooses which subset to display prominently — typically"VERIFIED"when verified ratings cross a threshold (Avatar 3:VERIFIEDselected with 10,000+ verified ratings out of 25,000+ total). The fulloverlay.audienceAllandoverlay.audienceVerifiedblocks are always present when both exist — read both and emit both subsets in the output JSON, not just the primary surface.certified: truemeans different things on each side: OncriticsScoreit's the classic "Certified Fresh" (≥75 % score + 80 reviews including 5 top-critic reviews). OnaudienceScoreit's the newer "Verified Hot" / "Certified Audience" tier (high verified-purchase rating). The field name is the same; the semantics aren't. Emit them as separatetomatometer_statusandaudience_statusfields.- No score yet ≠ zero score: A pre-release / under-reviewed title shows
criticsScore: {likedCount:0, notLikedCount:0, ratingCount:0, reviewCount:0, title:"Tomatometer"}with noscore/sentimentfield. Don't coerce missing to0— emitnull(or omit) and set status to"No score yet". Same applies on the audience side (reviewCount:0, noscore,certifiedFresh:"none"). - JSON-LD
actor[]has no character names: It only carries actor names + RT celebrity URLs + headshots. Cast roles ("Neo", "Morpheus", "Trinity") live in the HTML cast tiles under<p class="role" data-qa="person-role">. If your output schema needs role names, you must parse the HTML — JSON-LD alone is not enough. - JSON-LD
descriptionis SEO copy: It reads"Discover reviews, ratings, and trailers for The Matrix on Rotten Tomatoes…"— meta-description fluff, not the actual synopsis. The real synopsis isj.descriptionon the scorecard JSON (orj.overlaypayload). Don't surface the LDdescriptionto users. dateCreatedsemantics shift by title type: For aMovieit's theatrical release. For aTVSeriesit's the original series premiere (e.g. Severance:2022-02-18). For aTVSeasonit's that season's premiere (Severance S2:2025-01-17). The HTMLRelease Date (Theaters)/Premiere Datemedia-info fields carry the same value in human-friendly form.bb fetchdoes not require--proxiesand is NOT rate-limited in normal usage: Verified across 7 consecutive fetches with no 403/429 from a US-region Browserbase egress. The page-level Akamai config is permissive for read paths. Reserve--proxiesand--verifiedfor genuine failure recovery; don't add them prophylactically (they're slower).- Search results are server-rendered web components, not JS-hydrated cards:
<search-page-media-row>attribute strings already contain the disambiguation signals (release-year,cast,tomatometer-score). You don't need a snapshot/eval/JS-hydration step — the raw HTML attributes are enough to pick the right row. - Free-form search with multiple top hits: A query like
"the matrix"returns 68 movie matches and ~11,700 TV matches (reboots, parodies, indie titles using the word). Always disambiguate byrelease-yearwhen the user gave a year, by media type when the user said "show" / "season" / "movie", or by cast intersection. If still ambiguous, return the top-3 candidates with their RT URLs and scores rather than guessing. - Some streaming affiliates are sponsored:
wtw.affiliates[].isSponsoredLink === trueflags paid placements (often Fandango at Home for older titles RT still owns). The user-meaningful affiliates (Netflix / Max / Disney+ / etc.) are non-sponsored. Filter or annotate accordingly when emittingstreaming_available_on. - READ-ONLY: Never click
Want to See,Not Interested,Sign In, the star-rating widgets in audience-review composer, or theSubmit your reviewbutton. The skill purpose is observation only.
Expected Output
Single object covering all three title types. Fields not applicable to the type are null or omitted.
{
"url": "https://www.rottentomatoes.com/m/matrix",
"slug": "m/matrix",
"title": "The Matrix",
"original_title": null,
"title_type": "movie",
"media_type_raw": "Movie",
"release_year": 1999,
"year_range": null,
"release_date_theaters": "1999-03-31",
"release_date_streaming": "2009-01-01",
"content_rating": "R",
"runtime_minutes": 136,
"runtime_display": "2h 16m",
"episode_count": null,
"season_count": null,
"genres": ["Sci-Fi", "Action", "Mystery & Thriller"],
"synopsis": "Neo believes that Morpheus, an elusive figure considered to be the most dangerous man alive, can answer his question -- What is the Matrix? …",
"poster_url": "https://resizing.flixster.com/…ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2EwMGEwNmQxLTE1MGYtNGQwYS04ZDhlLWQ0MzYwOTQ5M2JlMC5qcGc=",
"studio": ["Warner Bros.", "Village Roadshow Prod.", "Silver Pictures"],
"distributor": "Warner Bros. Pictures",
"tomatometer": {
"score": 83,
"status": "Certified Fresh",
"sentiment": "POSITIVE",
"certified": true,
"rating_count": 209,
"review_count": 209,
"average_rating": 7.9,
"liked_count": 173,
"not_liked_count": 36,
"reviews_page_url": "https://www.rottentomatoes.com/m/matrix/reviews",
"top_critics": {
"score": 71, "rating_count": 58, "certified": true,
"reviews_page_url": "https://www.rottentomatoes.com/m/matrix/reviews/top-critics"
}
},
"popcornmeter": {
"score": 85,
"status": "Upright",
"sentiment": "POSITIVE",
"certified": false,
"score_type": "ALL",
"review_count": 1307885,
"banded_rating_count": "250,000+ Ratings",
"average_rating": 3.6,
"liked_count": 142778,
"not_liked_count": 24632,
"reviews_page_url": "https://www.rottentomatoes.com/m/matrix/reviews/all-audience",
"verified_only": null
},
"critics_consensus": "Thanks to the Wachowskis' imaginative vision, The Matrix is a smartly crafted combination of spectacular action and groundbreaking special effects.",
"directors": [
{"name": "Lilly Wachowski", "url": "https://www.rottentomatoes.com/celebrity/lilly_wachowski"},
{"name": "Lana Wachowski", "url": "https://www.rottentomatoes.com/celebrity/lana_wachowski"}
],
"writers": [],
"cast": [
{"name": "Keanu Reeves", "role": "Thomas \"Neo\" Anderson", "url": "https://www.rottentomatoes.com/celebrity/keanu_reeves"},
{"name": "Laurence Fishburne", "role": "Morpheus", "url": "https://www.rottentomatoes.com/celebrity/larry_fishburne"},
{"name": "Carrie-Anne Moss", "role": "Trinity", "url": "https://www.rottentomatoes.com/celebrity/carrie_anne_moss"},
{"name": "Hugo Weaving", "role": "Agent Smith", "url": "https://www.rottentomatoes.com/celebrity/hugo_weaving"}
],
"critic_reviews_sample": [
{
"critic": "Joe Morgenstern",
"publication": "Wall Street Journal",
"is_top_critic": true,
"sentiment": "POSITIVE",
"rating": "2.5/4",
"date": "07/13/2023",
"quote": "Though The Matrix ultimately overdoses on gloom-and-doom grunge…",
"original_review_url": "https://web.archive.org/web/19990508122457/http://www.usatoday.com/life/enter/movies/lfilm598.htm",
"critic_url": "https://www.rottentomatoes.com/critic/joe-morgenstern"
}
],
"streaming_available_on": [
{"name": "Fandango at Home", "icon": "fandango-at-home", "url": "https://athome.fandango.com/content/browse/details/The-Matrix/9254?cmp=rt_where_to_watch", "is_sponsored": false}
],
"has_showtimes": false,
"showtimes_url": null
}
TV series (series-wide aggregate)
{
"url": "https://www.rottentomatoes.com/tv/severance",
"slug": "tv/severance",
"title": "Severance",
"title_type": "tvSeries",
"media_type_raw": "TvSeries",
"release_year": 2022,
"year_range": "2022–",
"content_rating": "TV-MA",
"season_count": 2,
"runtime_minutes": null,
"tomatometer": { "score": 95, "status": "Fresh", "is_average": true, "rating_count": 242, "average_rating": 8.7 },
"popcornmeter": { "score": 80, "status": "Upright", "is_average": true, "banded_rating_count": "5,000+ Ratings" },
"seasons": [
{"season_number": 1, "url": "https://www.rottentomatoes.com/tv/severance/s01"},
{"season_number": 2, "url": "https://www.rottentomatoes.com/tv/severance/s02"}
]
}
TV season (specific season's actual score)
{
"url": "https://www.rottentomatoes.com/tv/severance/s02",
"slug": "tv/severance/s02",
"title": "Severance: Season 2",
"title_type": "tvSeason",
"media_type_raw": "TvSeason",
"season_number": 2,
"parent_series": {
"slug": "tv/severance",
"title": "Severance",
"url": "https://www.rottentomatoes.com/tv/severance",
"series_start_date": "2022-02-18"
},
"premiere_date": "2025-01-17",
"content_rating": "TV-MA",
"tomatometer": { "score": 94, "status": "Certified Fresh", "certified": true, "is_average": false, "rating_count": 228 },
"popcornmeter": { "score": 74, "status": "Upright", "is_average": false, "banded_rating_count": "5,000+ Ratings" }
}
No-score-yet (pre-release)
{
"url": "https://www.rottentomatoes.com/m/the_odyssey_2026",
"title": "The Odyssey",
"title_type": "movie",
"release_year": 2026,
"tomatometer": { "score": null, "status": "No score yet", "rating_count": 0 },
"popcornmeter": { "score": null, "status": "No score yet", "review_count": 0 }
}
Free-form-title disambiguation (multiple matches)
{
"success": false,
"reason": "ambiguous_title",
"query": "the matrix",
"candidates": [
{"title": "The Matrix", "type": "movie", "release_year": 1999, "url": "https://www.rottentomatoes.com/m/matrix", "tomatometer_score": 83},
{"title": "The Matrix Resurrections", "type": "movie", "release_year": 2021, "url": "https://www.rottentomatoes.com/m/the_matrix_resurrections", "tomatometer_score": 63},
{"title": "The Matrix Reloaded","type": "movie", "release_year": 2003, "url": "https://www.rottentomatoes.com/m/matrix_reloaded","tomatometer_score": 73}
]
}