Skip to main content
No auth required. Trending data is public — send the standard required URL parameters (client_id, app-name, app-version) and a User-Agent header, but no user Authorization token.
Which IDs can I send/expect? All accepted input identifiers and the keys you’ll see echoed back in responses are listed at Standard media objects → Supported ID keys. Send every ID you have on writes — Simkl picks the first that resolves and ignores the rest. Reminder: slug is response-only (never send it on a request).
Attribution required. When you display trending data in your app or website, the section title must include Simkl alongside Trending (or an equivalent like Most Watched / Popular). Any sensible combination works — feel free to invent your own wording, as long as both ideas appear together. A few examples to get you started:
Simkl Trending MoviesTrending Movies on SimklWhat's Trending on Simkl
Simkl Trending TV ShowsTrending TV on SimklNow Trending — Simkl
Simkl Trending AnimeTrending Anime on SimklTrending Now · Powered by Simkl
Simkl Trending TodayTrending Today on SimklToday on Simkl
Simkl Trending This WeekTrending This Week on SimklThis Week's Top Picks — Simkl
Simkl Most WatchedMost Watched on SimklPopular on Simkl
Simkl Top 100 MoviesTop 100 on SimklHot Right Now — Simkl
Simkl Top ChartsTop Charts on SimklCharting on Simkl
For commercial use without attribution, contact us — we’re happy to discuss licensing.

Linking back (websites only)

If your client can render hyperlinks — websites, browser extensions, web apps — link the title to the matching Simkl Most Watched page. TV apps, consoles, CLIs, and other contexts that can’t open external URLs are exempt; the title alone is enough.

Most Watched Movies

simkl.com/movies/best-movies/most-watched

Most Watched TV

simkl.com/tv/best-shows/most-watched

Most Watched Anime

simkl.com/anime/best-anime/most-watched
Simkl provides pre-built JSON files with trending data ranked by the number of watchers. These are the same rankings displayed on Simkl’s Most Watched pages for Movies, TV Shows, and Anime. Each file is available in two sizes: top 100 (_100) or top 500 (_500) items. Titles with the most watchers are returned first.

At a glance

Sizes

Top 100 (_100) and Top 500 (_500) per file.

Last-Modified

Each file’s Last-Modified response header tells you exactly when it was generated.

Update frequency

DataDescriptionUpdate frequency
TodayMost watched titles over the last 24 hoursEvery hour
WeekMost watched titles over the last 7 daysOnce a day
MonthMost watched titles over the last 30 daysOnce a day
DVD releasesLatest popular DVD releases (Movies only)Once a day
Regeneration is best-effort, not strictly precise. The cadences above are targets — the actual job can take a few extra minutes (and occasionally longer) to finish. If your scheduled refresh hits a 304 Not Modified right at the expected time, the new file just hasn’t been generated yet; back off and retry in a few minutes rather than waiting another full cycle. See SimklTrendingClient — drop-in SDK for the recommended pattern.
The file URLs ignore all query strings. Don’t add ?random=... or ?nocache=... — the CDN treats every variant as the same resource. Simkl regenerates the files on the schedule above and automatically clears them from the Cloudflare cache, so you’ll always get the latest version on your next request — client-side cache-busting won’t deliver newer data.
Append ?client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0 to every URL below — the SimklTrendingClient does this for you from CONFIG; if you’re rolling your own, see Without the SDK.

Combined

Movies + TV + anime in a single response. Top-level keys: movies, tv, anime. Best when you want all categories at once and don’t want three round-trips.

By type — Movies, TV, Anime

One category per file. Top-level response is the array directly (no wrapper object). Best when you only render one category, or want to fetch them independently.

DVD releases (movies only)

Latest popular DVD releases — mirrors Simkl DVD Releases. No timeframe split; refreshes once a day.

Copy-paste JSON

Drop this object into your app as a constant. Replace YOUR_CLIENT_ID with your registered app’s client_id and my-app-name / 1.0 with your app’s name and version — both the URL params (required_query) AND the matching User-Agent header (user_agent_header) are required on every request, free or paid plan. Each entry has a title (attribution-compliant display label), a url (bare path — append ? + required_query when you fetch), and a refresh_seconds (how often the file is regenerated server-side — set your refresh timer to this value). The structure is category → timeframe → size so you can filter or iterate by any axis. DVD has no timeframe.
{
  "required_query":    "client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0",
  "user_agent_header": "my-app-name/1.0",
  "urls": {
    "combined": {
      "today": {
        "top_100": { "title": "Trending on Simkl — Today (Top 100)",      "url": "https://data.simkl.in/discover/trending/today_100.json",  "refresh_seconds": 3600 },
        "top_500": { "title": "Trending on Simkl — Today (Top 500)",      "url": "https://data.simkl.in/discover/trending/today_500.json",  "refresh_seconds": 3600 }
      },
      "week": {
        "top_100": { "title": "Trending on Simkl — This Week (Top 100)",  "url": "https://data.simkl.in/discover/trending/week_100.json",   "refresh_seconds": 86400 },
        "top_500": { "title": "Trending on Simkl — This Week (Top 500)",  "url": "https://data.simkl.in/discover/trending/week_500.json",   "refresh_seconds": 86400 }
      },
      "month": {
        "top_100": { "title": "Trending on Simkl — This Month (Top 100)", "url": "https://data.simkl.in/discover/trending/month_100.json",  "refresh_seconds": 86400 },
        "top_500": { "title": "Trending on Simkl — This Month (Top 500)", "url": "https://data.simkl.in/discover/trending/month_500.json",  "refresh_seconds": 86400 }
      }
    },
    "movies": {
      "today": {
        "top_100": { "title": "Trending Movies on Simkl — Today (Top 100)",      "url": "https://data.simkl.in/discover/trending/movies/today_100.json",  "refresh_seconds": 3600 },
        "top_500": { "title": "Trending Movies on Simkl — Today (Top 500)",      "url": "https://data.simkl.in/discover/trending/movies/today_500.json",  "refresh_seconds": 3600 }
      },
      "week": {
        "top_100": { "title": "Trending Movies on Simkl — This Week (Top 100)",  "url": "https://data.simkl.in/discover/trending/movies/week_100.json",   "refresh_seconds": 86400 },
        "top_500": { "title": "Trending Movies on Simkl — This Week (Top 500)",  "url": "https://data.simkl.in/discover/trending/movies/week_500.json",   "refresh_seconds": 86400 }
      },
      "month": {
        "top_100": { "title": "Trending Movies on Simkl — This Month (Top 100)", "url": "https://data.simkl.in/discover/trending/movies/month_100.json",  "refresh_seconds": 86400 },
        "top_500": { "title": "Trending Movies on Simkl — This Month (Top 500)", "url": "https://data.simkl.in/discover/trending/movies/month_500.json",  "refresh_seconds": 86400 }
      }
    },
    "tv": {
      "today": {
        "top_100": { "title": "Trending TV Shows on Simkl — Today (Top 100)",      "url": "https://data.simkl.in/discover/trending/tv/today_100.json",  "refresh_seconds": 3600 },
        "top_500": { "title": "Trending TV Shows on Simkl — Today (Top 500)",      "url": "https://data.simkl.in/discover/trending/tv/today_500.json",  "refresh_seconds": 3600 }
      },
      "week": {
        "top_100": { "title": "Trending TV Shows on Simkl — This Week (Top 100)",  "url": "https://data.simkl.in/discover/trending/tv/week_100.json",   "refresh_seconds": 86400 },
        "top_500": { "title": "Trending TV Shows on Simkl — This Week (Top 500)",  "url": "https://data.simkl.in/discover/trending/tv/week_500.json",   "refresh_seconds": 86400 }
      },
      "month": {
        "top_100": { "title": "Trending TV Shows on Simkl — This Month (Top 100)", "url": "https://data.simkl.in/discover/trending/tv/month_100.json",  "refresh_seconds": 86400 },
        "top_500": { "title": "Trending TV Shows on Simkl — This Month (Top 500)", "url": "https://data.simkl.in/discover/trending/tv/month_500.json",  "refresh_seconds": 86400 }
      }
    },
    "anime": {
      "today": {
        "top_100": { "title": "Trending Anime on Simkl — Today (Top 100)",      "url": "https://data.simkl.in/discover/trending/anime/today_100.json",  "refresh_seconds": 3600 },
        "top_500": { "title": "Trending Anime on Simkl — Today (Top 500)",      "url": "https://data.simkl.in/discover/trending/anime/today_500.json",  "refresh_seconds": 3600 }
      },
      "week": {
        "top_100": { "title": "Trending Anime on Simkl — This Week (Top 100)",  "url": "https://data.simkl.in/discover/trending/anime/week_100.json",   "refresh_seconds": 86400 },
        "top_500": { "title": "Trending Anime on Simkl — This Week (Top 500)",  "url": "https://data.simkl.in/discover/trending/anime/week_500.json",   "refresh_seconds": 86400 }
      },
      "month": {
        "top_100": { "title": "Trending Anime on Simkl — This Month (Top 100)", "url": "https://data.simkl.in/discover/trending/anime/month_100.json",  "refresh_seconds": 86400 },
        "top_500": { "title": "Trending Anime on Simkl — This Month (Top 500)", "url": "https://data.simkl.in/discover/trending/anime/month_500.json",  "refresh_seconds": 86400 }
      }
    },
    "dvd": {
      "top_100": { "title": "Latest DVD Releases on Simkl (Top 100)", "url": "https://data.simkl.in/discover/dvd/releases_100.json", "refresh_seconds": 86400 },
      "top_500": { "title": "Latest DVD Releases on Simkl (Top 500)", "url": "https://data.simkl.in/discover/dvd/releases_500.json", "refresh_seconds": 86400 }
    }
  },
  "catalogs": [
    { "id": "movies_today",           "source": "movies", "timeframe": "today" },
    { "id": "tv_today",               "source": "tv",     "timeframe": "today" },
    { "id": "anime_today",            "source": "anime",  "timeframe": "today" },
    { "id": "dvd",                    "source": "dvd"                          },

    { "id": "tv_premieres",           "source": "tv",     "title": "Trending TV Premieres on Simkl (Last 30 Days)",     "recipe": "justReleased", "recipe_opts": { "withinDays": 30 } },
    { "id": "movies_premieres",       "source": "movies", "title": "Trending Movie Premieres on Simkl (Last 30 Days)",  "recipe": "justReleased", "recipe_opts": { "withinDays": 30 } },
    { "id": "tv_ongoing",             "source": "tv",     "title": "Currently Airing TV on Simkl",           "recipe": "ongoing" },

    { "id": "movies_this_year",       "source": "movies", "title": "Movies This Year on Simkl",              "recipe": "currentYear" },
    { "id": "tv_this_year",           "source": "tv",     "title": "TV This Year on Simkl",                  "recipe": "currentYear" },
    { "id": "anime_this_year",        "source": "anime",  "title": "Anime This Year on Simkl",               "recipe": "currentYear" },
    { "id": "movies_last_year",       "source": "movies", "title": "Movies of Last Year on Simkl",           "recipe": "lastYear" },
    { "id": "tv_last_year",           "source": "tv",     "title": "TV of Last Year on Simkl",               "recipe": "lastYear" },

    { "id": "movies_anticipated",     "source": "movies", "title": "Most Watchlisted Movies on Simkl",       "recipe": "mostWatchlisted" },
    { "id": "tv_anticipated",         "source": "tv",     "title": "Most Watchlisted TV on Simkl",           "recipe": "mostWatchlisted" },
    { "id": "anime_anticipated",      "source": "anime",  "title": "Most Watchlisted Anime on Simkl",        "recipe": "mostWatchlisted" },

    { "id": "movies_top_rated",       "source": "movies", "title": "Top Rated Movies on Simkl",              "recipe": "highestRated" },
    { "id": "tv_top_rated",           "source": "tv",     "title": "Top Rated TV on Simkl",                  "recipe": "highestRated" },
    { "id": "anime_top_rated",        "source": "anime",  "title": "Top Rated Anime on Simkl",               "recipe": "highestRated",  "recipe_opts": { "source": "mal" } },
    { "id": "movies_by_rank",         "source": "movies", "title": "All-Time Best Movies on Simkl",          "recipe": "byRank" },
    { "id": "tv_by_rank",             "source": "tv",     "title": "All-Time Best TV on Simkl",              "recipe": "byRank" },
    { "id": "anime_by_rank",          "source": "anime",  "title": "All-Time Best Anime on Simkl",           "recipe": "byRank" },
    { "id": "movies_box_office",      "source": "movies", "title": "Top Box Office Movies on Simkl",         "recipe": "bestBoxOffice" },

    { "id": "movies_hidden_gems",     "source": "movies", "title": "Hidden Gem Movies on Simkl",             "recipe": "hiddenGems" },
    { "id": "tv_hidden_gems",         "source": "tv",     "title": "Hidden Gem TV on Simkl",                 "recipe": "hiddenGems" },
    { "id": "anime_hidden_gems",      "source": "anime",  "title": "Hidden Gem Anime on Simkl",              "recipe": "hiddenGems",    "recipe_opts": { "source": "mal" } },
    { "id": "tv_marathon",            "source": "tv",     "title": "Marathon-Worthy TV on Simkl",            "recipe": "marathonWorthy" },
    { "id": "movies_quick",           "source": "movies", "title": "Quick Watches on Simkl (≤ 90 min)",      "recipe": "quickWatches" },

    { "id": "tv_best_netflix",        "source": "tv",     "title": "Best of Netflix on Simkl",               "recipe": "bestOfNetwork", "recipe_opts": { "network": "Netflix" } },
    { "id": "tv_best_hbo",            "source": "tv",     "title": "Best of HBO on Simkl",                   "recipe": "bestOfNetwork", "recipe_opts": { "network": "HBO" } },
    { "id": "tv_best_disney",         "source": "tv",     "title": "Best of Disney+ on Simkl",               "recipe": "bestOfNetwork", "recipe_opts": { "network": "Disney+" } },
    { "id": "tv_best_prime",          "source": "tv",     "title": "Best of Prime Video on Simkl",           "recipe": "bestOfNetwork", "recipe_opts": { "network": "Prime Video" } },
    { "id": "tv_best_apple",          "source": "tv",     "title": "Best of Apple TV on Simkl",              "recipe": "bestOfNetwork", "recipe_opts": { "network": "Apple TV" } },

    { "id": "movies_best_action",     "source": "movies", "title": "Best Action Movies on Simkl",            "recipe": "bestOfGenre",   "recipe_opts": { "genre": "Action" } },
    { "id": "movies_best_drama",      "source": "movies", "title": "Best Drama Movies on Simkl",             "recipe": "bestOfGenre",   "recipe_opts": { "genre": "Drama" } },
    { "id": "movies_best_comedy",     "source": "movies", "title": "Best Comedy Movies on Simkl",            "recipe": "bestOfGenre",   "recipe_opts": { "genre": "Comedy" } },
    { "id": "movies_best_scifi",      "source": "movies", "title": "Best Sci-Fi Movies on Simkl",            "recipe": "bestOfGenre",   "recipe_opts": { "genre": "Science Fiction" } },
    { "id": "movies_best_thriller",   "source": "movies", "title": "Best Thriller Movies on Simkl",          "recipe": "bestOfGenre",   "recipe_opts": { "genre": "Thriller" } },
    { "id": "movies_best_horror",     "source": "movies", "title": "Best Horror Movies on Simkl",            "recipe": "bestOfGenre",   "recipe_opts": { "genre": "Horror" } },
    { "id": "tv_best_drama",          "source": "tv",     "title": "Best Drama TV on Simkl",                 "recipe": "bestOfGenre",   "recipe_opts": { "genre": "Drama" } },
    { "id": "tv_best_comedy",         "source": "tv",     "title": "Best Comedy TV on Simkl",                "recipe": "bestOfGenre",   "recipe_opts": { "genre": "Comedy" } },
    { "id": "tv_best_crime",          "source": "tv",     "title": "Best Crime TV on Simkl",                 "recipe": "bestOfGenre",   "recipe_opts": { "genre": "Crime" } },
    { "id": "tv_best_scifi",          "source": "tv",     "title": "Best Sci-Fi TV on Simkl",                "recipe": "bestOfGenre",   "recipe_opts": { "genre": "Science-Fiction" } },
    { "id": "anime_best_shounen",     "source": "anime",  "title": "Best Shounen Anime on Simkl",            "recipe": "bestOfGenre",   "recipe_opts": { "genre": "Shounen", "source": "mal" } },
    { "id": "anime_best_isekai",      "source": "anime",  "title": "Best Isekai Anime on Simkl",             "recipe": "bestOfGenre",   "recipe_opts": { "genre": "Isekai",  "source": "mal" } },
    { "id": "anime_best_romance",     "source": "anime",  "title": "Best Romance Anime on Simkl",            "recipe": "bestOfGenre",   "recipe_opts": { "genre": "Romance", "source": "mal" } },

    { "id": "movies_best_2020s",      "source": "movies", "title": "Best Movies of the 2020s on Simkl",      "recipe": "bestOfDecade",  "recipe_opts": { "decade": 2020 } },
    { "id": "movies_best_2010s",      "source": "movies", "title": "Best Movies of the 2010s on Simkl",      "recipe": "bestOfDecade",  "recipe_opts": { "decade": 2010 } },
    { "id": "movies_best_2000s",      "source": "movies", "title": "Best Movies of the 2000s on Simkl",      "recipe": "bestOfDecade",  "recipe_opts": { "decade": 2000 } },
    { "id": "movies_best_90s",        "source": "movies", "title": "Best Movies of the 90s on Simkl",        "recipe": "bestOfDecade",  "recipe_opts": { "decade": 1990 } },
    { "id": "tv_best_2020s",          "source": "tv",     "title": "Best TV of the 2020s on Simkl",          "recipe": "bestOfDecade",  "recipe_opts": { "decade": 2020 } },
    { "id": "tv_best_2010s",          "source": "tv",     "title": "Best TV of the 2010s on Simkl",          "recipe": "bestOfDecade",  "recipe_opts": { "decade": 2010 } },
    { "id": "anime_best_2020s",       "source": "anime",  "title": "Best Anime of the 2020s on Simkl",       "recipe": "bestOfDecade",  "recipe_opts": { "decade": 2020, "source": "mal" } },
    { "id": "anime_best_2010s",       "source": "anime",  "title": "Best Anime of the 2010s on Simkl",       "recipe": "bestOfDecade",  "recipe_opts": { "decade": 2010, "source": "mal" } },

    { "id": "anime_movies",           "source": "anime",  "title": "Trending Anime Movies on Simkl",         "recipe": "byAnimeType",   "recipe_opts": { "animeType": "movie" } },
    { "id": "anime_ovas",             "source": "anime",  "title": "Trending Anime OVAs on Simkl",           "recipe": "byAnimeType",   "recipe_opts": { "animeType": "ova" } }
  ]
}
  • Titles are pre-composed to satisfy the attribution requirement (every label includes both “Trending” / “Most Watched” and “Simkl”). Use them as-is or adapt the wording — just keep both halves.
  • catalogs is the default row list the SDK renders on a home screen — 44 rows out of the box, all titled and ready to display. Mix of: trending (movies/tv/anime today + DVD), sort variants (byRank, highestRated, mostAnticipated × 3 types), composed catalogs (hiddenGems × 3, criticallyAcclaimed × 2, marathonWorthy, bestBoxOffice), Best of (Netflix / HBO / Disney+ / Prime Video / Apple TV), Best of for movies (Action / Sci-Fi / Comedy / Horror / Thriller / Drama), TV (Drama / Comedy / Crime / Sci-Fi), anime (Shounen / Isekai / Romance), and time-based filters (Quick Watches, In Theaters Now, Just Released on DVD, Currently Airing TV, Anime Movies, Anime OVAs). Edit the JSON to remove rows you don’t want, change titles, or swap recipe opts. Per-app override still works via cfg.catalogs in the SDK config. Currently the recipes ship in the Node + Browser tabs below; Python / Swift / Kotlin / Dart will render the derived rows as raw items until those tabs gain recipe ports (or you supply a transform closure).
  • refresh_seconds is 3600 (1 hour) for today files and 86400 (1 day) for everything else, matching the Update frequency table. This is the nominal regeneration cadence, not a guarantee — server regeneration can take a few minutes longer. For bandwidth-optimal refresh, pair this with the response’s Last-Modified header and send If-Modified-Since on subsequent requests — the server returns 304 Not Modified when nothing has changed. If a refresh scheduled for refresh_seconds returns 304, back off and retry rather than assuming the cache is final (see SimklTrendingClient — drop-in SDK).
Using the SDK below? Save this whole object as simkl-trending-urls.json in your project — the SDK class loads it back into a SIMKL_TRENDING_URLS symbol. Each language tab’s top comment shows the runtime-appropriate one-liner.

SimklTrendingClient — drop-in SDK

Two-step setup before pasting the SDK class below. First, save the Copy-paste JSON block above as simkl-trending-urls.json in your project (or as an asset your runtime can load). Second, load it back into a SIMKL_TRENDING_URLS symbol — each tab’s top comment shows the runtime-appropriate one-liner (import / require / fetch for JS, open() for Python, Bundle.main for Swift, context.assets for Android, rootBundle for Flutter). The constructor throws if neither this symbol nor an explicit urls argument is in scope.
A single class that wires up the entire data flow. Tune the CONFIG block at the top of each tab — everything else is plumbing you should not need to edit. What’s built in:
  • Auto-picks the Combined endpoint when you ask for 2+ of movies/tv/anime — one HTTP call per timeframe instead of two or three.
  • Persistent cache keyed on the URL — relaunch can paint the UI instantly from the last persisted snapshot before any network call fires. JS auto-detects the host (browser → localStorage[storageKey], Node → file at persistPath); other languages use platform-native paths. Cache entries older than persistMaxAgeSeconds are dropped on restore. client.clearCache() wipes both in-memory and persistent state (and notifies subscribers so the UI repaints empty). For server-side runtimes with many processes, swap the _persist / _restorePersisted / clearCache trio for a Redis / KV adapter.
  • Conditional revalidation with If-Modified-Since so refreshes hit the CDN with a cheap 304 when nothing has changed.
  • Regen-lag backoff — when the server hasn’t regenerated by the time refresh_seconds elapses, the client backs off via a [5min, 15min, 30min] ladder instead of hammering on the nominal cadence.
  • Jitter — ±5% random jitter on every refresh + ±10% on retry delays, so all clients don’t hit the CDN at exactly :00 and don’t retry-storm in lockstep.
  • Retry with exponential backoff + per-request timeout + Retry-After honored for network blips and 429s.
  • Permanent-error classification404 / 400 / 401 / 403 are not retried (they’d waste the backoff schedule on a request that will never succeed); the entry’s loop halts and you see the error in the log.
  • Process-wide minIntervalMs floor — N entries refreshing at the same tick are serialized through a 250 ms (configurable) gate so you never burst N parallel requests at the CDN.
  • Concurrent fetch dedup — two callers asking for the same URL fold into one in-flight request.
  • Cancellationclient.stop() aborts every in-flight request and clears every timer.
  • Subscribe / get — your UI subscribes to a type and receives the current cached items immediately, plus a callback on every successful refresh. get(type) is synchronous — paginate the returned array locally for UI pages instead of refetching.
  • client.tick() for background schedulers — call from a BGTaskScheduler / WorkManager / workmanager / cron periodic task to perform a single revalidate-and-persist pass.
  • Namespace-safe — every public symbol is prefixed Simkl… (SimklTrendingClient, SimklTrendingConfig, SimklTrendingError, SIMKL_TRENDING_URLS, etc.) so dropping the class into an existing app doesn’t collide with your own Config / Client / Entry types.
The only thing left to wire up per-platform is background scheduling (the OS-level periodic task that keeps the cache warm while your app is suspended) — see the comment at the bottom of each tab.

Try it live

A self-contained vanilla-JS demo runs in CodePen below — same SDK, all 55 default catalog rows, two live fetches against data.simkl.in: trending/today_500.json (combined movies/TV/anime in one round-trip) and dvd/releases_500.json. Click Result to see the rendered home screen; JS / HTML / CSS tabs show the wiring.

Open & fork in CodePen

Edit the catalog config, try other recipes, swap the test client_id for your own.
<!--
  Save this whole file as `index.html` next to two siblings:
    simkl-trending-urls.json    ← the "Copy-paste JSON" block above
    simkl-trending-client.js    ← the "JavaScript (save as simkl-trending-client.js)" tab →
  Serve over http:// (ES modules don't load over file://). For local dev: `python -m http.server`.
  No build step, no framework — what you see is what you get.
-->
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Simkl Trending</title>
  <style>
    :root { color-scheme: dark; }
    body { margin: 0; background: #0b0d12; color: #e6e8ee; font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; }
    header { padding: 22px 26px 14px; border-bottom: 1px solid #1d2330; }
    header h1 { margin: 0 0 4px; font-size: 18px; letter-spacing: .02em; }
    header .sub { color: #8a93a6; font-size: 13px; }
    main { padding: 18px 0 60px; }
    .row { padding: 18px 0 6px; }
    .row h2 { margin: 0 0 10px; padding: 0 26px; font-size: 15px; font-weight: 600; color: #d6dbe8; }
    .row h2 .count { color: #6b7280; font-weight: 400; font-size: 12px; margin-left: 8px; }
    .scroller { display: flex; gap: 12px; overflow-x: auto; padding: 4px 26px 14px; scrollbar-width: thin; }
    .scroller::-webkit-scrollbar { height: 8px; }
    .scroller::-webkit-scrollbar-thumb { background: #2a3142; border-radius: 4px; }
    .card { flex: 0 0 auto; width: 132px; cursor: pointer; }
    .card .poster { width: 132px; height: 198px; background: #1d2330 center/cover no-repeat; border-radius: 6px; overflow: hidden; position: relative; }
    .card .poster .rank { position: absolute; top: 6px; left: 6px; background: rgba(0,0,0,.7); color: #fff; font-size: 11px; padding: 2px 6px; border-radius: 3px; }
    .card .poster .rating { position: absolute; bottom: 6px; right: 6px; background: rgba(0,0,0,.7); color: #f7c948; font-size: 11px; padding: 2px 6px; border-radius: 3px; }
    .card .title { margin-top: 6px; font-size: 12px; line-height: 1.3; color: #d6dbe8; max-height: 32px; overflow: hidden; }
    .card .meta { font-size: 11px; color: #8a93a6; margin-top: 2px; }
    .empty { padding: 4px 26px 14px; color: #6b7280; font-size: 12px; font-style: italic; }
    .toolbar { padding: 14px 26px; color: #8a93a6; font-size: 12px; border-top: 1px solid #1d2330; background: #0e1117; position: sticky; bottom: 0; }
    .toolbar button { background: #1d2330; color: #d6dbe8; border: 1px solid #2a3142; padding: 6px 10px; border-radius: 4px; cursor: pointer; font: inherit; }
    .toolbar button:hover { background: #232a3a; }
    a, a:visited { color: #6ea8fe; }
  </style>
</head>

<body>
  <header>
    <h1>Trending on Simkl</h1>
    <div class="sub">Loads <code>simkl-trending-urls.json</code>, wires up <code>SimklTrendingClient</code>, subscribes to all configured catalogs, renders one carousel per row.</div>
  </header>

  <main id="rows"></main>

  <footer class="toolbar">
    <span id="status">booting…</span>
    &nbsp;·&nbsp;
    <button id="refresh">Refresh now</button>
    &nbsp;·&nbsp;
    <button id="clearcache">Clear cache</button>
    &nbsp;·&nbsp;
    Cache backend: <span id="cacheBackend"></span>
  </footer>

  <script type="module">
    import { SimklTrendingClient } from './simkl-trending-client.js';

    // ─── Render config — tweak here, not down inside render() ──────────────────
    const MAX_ITEMS_PER_ROW = 50;   // posters per carousel row (each row can hold hundreds; this just caps DOM cost)

    const $rows   = document.getElementById('rows');
    const $status = document.getElementById('status');
    document.getElementById('cacheBackend').textContent = (typeof localStorage !== 'undefined') ? 'localStorage' : '(none)';

    // 1. Load the URL catalog (JSON catalog + 44 default rows).
    const data = await (await fetch('./simkl-trending-urls.json')).json();

    // 2. Pull CONFIG out of the JSON so one file drives both URLs and client_id / app-name / app-version.
    const cfg = Object.fromEntries(new URLSearchParams(data.required_query));

    // 3. Instantiate. Defaults pull supportedTypes from SDK CONFIG, catalogs from JSON's catalogs[] array.
    const client = new SimklTrendingClient({
      clientId:   cfg.client_id,
      appName:    cfg['app-name'],
      appVersion: cfg['app-version'],
      userAgent:  data.user_agent_header,    // browsers ignore this anyway — set for non-browser runtimes
      size:       500,
    }, data);

    // 4. Render — diff-by-id so unchanged rows aren't rebuilt on every refresh.
    function render(rows) {
      for (const row of rows) {
        let el = document.getElementById(`row-${row.id}`);
        if (!el) {
          el = document.createElement('section');
          el.id = `row-${row.id}`;
          el.className = 'row';
          el.innerHTML = `<h2>${escapeHtml(row.title)} <span class="count"></span></h2><div class="scroller"></div>`;
          $rows.appendChild(el);
        }
        el.querySelector('.count').textContent = `(${row.items.length})`;
        const $sc = el.querySelector('.scroller');
        if (row.items.length === 0) {
          $sc.innerHTML = '';
          if (!el.querySelector('.empty')) {
            const m = document.createElement('div'); m.className = 'empty';
            m.textContent = 'no items yet — waiting for first fetch';
            el.appendChild(m);
          }
        } else {
          el.querySelector('.empty')?.remove();
          $sc.innerHTML = row.items.slice(0, MAX_ITEMS_PER_ROW).map(itemHtml).join('');
        }
      }
      $status.textContent = `${rows.length} rows · ${rows.reduce((s, r) => s + r.items.length, 0)} items total · last update ${new Date().toLocaleTimeString()}`;
    }

    function itemHtml(item) {
      const posterUrl = item.poster
        ? `https://wsrv.nl/?url=https://simkl.in/posters/${item.poster}_m.webp&q=90`
        : 'https://simkl.in/poster_no_pic.png';
      const ratingTxt = item.ratings?.simkl?.rating
        ? `★ ${item.ratings.simkl.rating.toFixed(1)}`
        : '';
      return `
        <a class="card" href="https://simkl.com${item.url ?? ''}" target="_blank" rel="noopener">
          <div class="poster" style="background-image:url('${posterUrl}')">
            ${item.rank ? `<span class="rank">#${item.rank}</span>` : ''}
            ${ratingTxt ? `<span class="rating">${ratingTxt}</span>` : ''}
          </div>
          <div class="title">${escapeHtml(item.title || '')}</div>
          <div class="meta">${escapeHtml(formatMeta(item))}</div>
        </a>
      `;
    }

    function formatMeta(item) {
      const year = (item.release_date || '').match(/(\d{4})/)?.[1];
      const bits = [];
      if (year)                              bits.push(year);
      if (item.runtime)                      bits.push(item.runtime);
      if (item.network)                      bits.push(item.network);
      if (item.country && !item.network)     bits.push(item.country.toUpperCase());
      return bits.join(' · ');
    }
    function escapeHtml(s) {
      return String(s).replace(/[&<>"']/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c]));
    }

    // 5. Subscribe — fires immediately from cache (if any), then on every refresh.
    client.subscribeCatalogs(render);

    // 6. Start. Boots fetches in the background.
    $status.textContent = 'starting…';
    await client.start();
    $status.textContent = 'running — refreshing on cadence';

    // 7. Wire toolbar.
    document.getElementById('refresh').addEventListener('click', () => {
      $status.textContent = 'refreshing…';
      client.tick()
        .then(() => { $status.textContent = 'refreshed via tick()'; })
        .catch(e => { $status.textContent = 'refresh failed: ' + e.message; });
    });
    document.getElementById('clearcache').addEventListener('click', async () => {
      $status.textContent = 'clearing cache…';
      await client.clearCache();
      $status.textContent = 'cache cleared — re-fetching…';
      client.tick()
        .then(() => { $status.textContent = 'cache cleared + re-fetched via tick()'; })
        .catch(e => { $status.textContent = 're-fetch failed: ' + e.message; });
    });

    window.simklTrending = client;   // exposed for console poking
  </script>
</body>
</html>
Node 22+, Deno, Bun, React Native, Cloudflare Workers, and other fetch-based JS runtimes — the JavaScript tab above (the SDK file) is universal: save it verbatim as simkl-trending-client.js, then wire it up from your app entry point in ~6 lines. The Browser tab right above it is the HTML+DOM wrapper you’d skip in a headless runtime; here’s the non-browser equivalent:
app.mjs (or index.js App.tsx)
// Pick the loader your runtime prefers:
import data from './simkl-trending-urls.json' with { type: 'json' };          // Node 22+ ESM
// const data = require('./simkl-trending-urls.json');                          // Node CommonJS
// const data = await (await fetch('/simkl-trending-urls.json')).json();        // Deno / Bun / RN / CF Workers
import { SimklTrendingClient } from './simkl-trending-client.js';

const client = new SimklTrendingClient(
  { clientId: 'YOUR_CLIENT_ID', appName: 'my-app', appVersion: '1.0' },
  data,
);
client.subscribeCatalogs(rows => rows.forEach(r => console.log(`${r.title}: ${r.items.length} items`)));
await client.start();
// process.on('SIGINT', () => { client.stop(); process.exit(0); });           // long-running servers
Bundle simkl-trending-urls.json with your app (or fetch + cache it once at startup) so the catalog is in scope when SimklTrendingClient runs. For Cloudflare Workers / Lambda / GitHub Actions etc., call await client.tick() from your scheduler instead of client.start() — see the “Background scheduling” comment at the bottom of the JavaScript tab.
What you still need to do per-platform. The SimklTrendingClient samples above wire up everything that’s host-agnostic — auto-pick combined, persistent cache, conditional revalidation, regen-lag backoff, jitter, retry with exponential backoff + timeout, in-flight dedup, cancellation, and subscribe/get. Two things are left to you because they depend on your platform:
  • Background scheduling that survives app lifecycle. In-process timers stop firing when an iOS app is backgrounded or after Android process death. Hook your platform’s periodic task system to client.tick()BGTaskScheduler (iOS), WorkManager.PeriodicWorkRequest (Android), the workmanager package (Flutter), or node-cron / Cloudflare Cron / AWS EventBridge for server-side runtimes. Each tab’s trailing comment shows the wireup.
  • Attribution back-link. Websites must link the rendered section title to the matching simkl.com/<type>/best-*/most-watched/ page — see Attribution required and Linking back.
If you want stronger typing on the per-item shape, the Field reference below shows the JSON, ready to bind to a TypeScript interface, Swift Codable, Kotlin @Serializable, or Dart json_serializable.

Catalogs — the rows on your home screen

Think of a TV-style discovery UI: rows of poster carousels stacked vertically, each row with a title at the top-left. The SDK exposes that as client.catalogs()[{id, title, items}, ...] and client.subscribeCatalogs(cb) — available in all 5 languages above. Each row has a stable id, an attribution-compliant title, and an items array. You iterate and render. Three places rows can come from, in priority order:
  1. cfg.catalogs in your SDK config — explicit, wins if set. Use this to mix trending + derived rows with custom transform closures (see below).
  2. catalogs[] in the Copy-paste JSON above — the default list of rows. Already populated with one row per type (movies_today, tv_today, anime_today, dvd). Add, remove, or reorder entries in the JSON to change defaults across your whole app — no code change.
  3. Auto-built fallback — if neither of the above is set, the SDK builds one row per supportedType × timeframes[0] from your CONFIG.
So with the JSON catalogs already populated, new SimklTrendingClient() immediately gives you 4 ready-to-render rows — no extra config needed. Override only when you want custom rows. Adding derived rows in code — when you want filter/sort logic that’s not in the JSON (e.g. “Hidden Gems”), pass cfg.catalogs with a transform function on each entry:
import { SimklTrendingClient, SimklTrendingRecipes as R } from './simkl-trending-client.js';

const client = new SimklTrendingClient({
  supportedTypes: ['movies', 'tv', 'anime'],
  size: 500,                                    // recommended once you derive rows
  catalogs: [
    { id: 'movies_today',       source: 'movies' },                                                                          // trending (JSON title)
    { id: 'tv_today',           source: 'tv' },
    { id: 'anime_today',        source: 'anime' },
    { id: 'movies_anticipated', source: 'movies', title: 'Most Watchlisted Movies on Simkl', transform: R.mostWatchlisted },
    { id: 'hidden_gems_anime',  source: 'anime',  title: 'Hidden Gem Anime on Simkl',        transform: items => R.hiddenGems(items, { minRank: 3000, source: 'mal' }) },
    { id: 'best_of_hbo',        source: 'tv',     title: 'Best of HBO — on Simkl',           transform: items => R.bestOfNetwork(items, 'HBO') },
  ],
});
client.subscribeCatalogs(rows => { for (const r of rows) renderRow(r.title, r.items); });
await client.start();
Need the rows synchronously (e.g. inside a render function)? client.catalogs() returns the resolved list on demand. Same {id, title, items} shape in every language. Available recipe transforms (drop into transform: to derive a row). Each has tunable opts as the 2nd arg — hiddenGems(items, { minRating, minVotes, minRank, source }), etc. Dial them to your taste without forking.
GroupRecipe names
SortsmostWatchlisted, highestRated, stickiest, newest, bestBoxOffice
FiltersbyGenre, byGenreAll, byCountry, byNetwork, byStatus, byAnimeType, withTrailer, quickWatches, marathonWorthy, miniSeries, justReleased, inTheatersNow, recentlyOnDvd, ongoing, recentlyEnded, justPremiered, byYear, byDecade, currentYear, lastYear
ComposedhiddenGems, criticallyAcclaimed, crossPlatformConsensus, massiveAnticipation, bestOfNetwork, bestOfGenre, bestOfDecade
GroupingsgroupByGenre, groupByCountry, groupByNetwork, groupByDecade (these return an object of buckets — convert to rows with Object.entries(R.groupByGenre(items)).map(([genre, items]) => ({ id: 'genre_' + genre, title: 'Trending ' + genre + ' on Simkl', items })))
Use size: 500 once you render derived rows. The top-100 file gives ~10-30 items per derived row at best (e.g. hiddenGems off today_100 movies typically yields 10-15 candidates). The top-500 file gives ~50-200 per row — enough to scroll horizontally. Same fetch schedule, ~3-5x larger file.
Adding / reordering rows costs nothing. Catalog rows are resolved at read time off the cached items. Changing cfg.catalogs and recomputing produces a new row list in microseconds. The SDK refreshes the underlying cache on its own cadence (~1h for today, 1d for week/month), independent of how many rows you render.
Yields on the live top-500 (May 2026 snapshot) — pick recipes to match your row-fill target: hiddenGems 67 movies / 156 TV / 204 anime · criticallyAcclaimed (default 8.5+ rating, 1k+ votes) 15 movies · marathonWorthy 75 TV · crossPlatformConsensus 66 movies · bestBoxOffice 449 movies parseable · quickWatches 38 movies under 90 min · byNetwork(tv, 'Netflix') 64 · byCountry(anime, 'jp') 361. Defaults are intentionally conservative — relax them if you want fewer empty rows.

Catalog ideas — recipe logic at a glance

The SimklTrendingRecipes namespace is shipped Node-only. If you’re porting to another language (the SDK class scaffolding above already covers Python / Swift / Kotlin / Dart), here’s the logic for each recipe — drop it into your transform: / client.get(type) pipeline and you have the row.
Catalog rowLogic
Most Watchlistedsort plan_to_watch desc — items that appear on the most user watchlists. The name “Most Anticipated” misled here: this fires for already-released titles too (huge backlog appeal — Game of Thrones, Avatar, One Piece all rank high). Use this for the universal “what’s on people’s minds” row.
Highest Rated (Simkl)sort ratings.simkl.rating desc, gate on ratings.simkl.votes >= 500 to drop noise
Highest Rated (IMDb / MAL)same, on ratings.imdb.rating (movies/TV) or ratings.mal.rating (anime)
Stickiestsort parseFloat(drop_rate) asc — viewers who started, finished, and didn’t bail
Newest releasessort by parsed release_date desc — release_date is MM/DD/YYYY (e.g. 03/26/2026), which new Date(...) / Python datetime.strptime("%m/%d/%Y") / Swift DateFormatter("MM/dd/yyyy") all accept
Box-office hits (movies)extract $NNNM from metadata, sort desc
Quick watches (movies)parse runtime to minutes, filter ≤ 90, sort by rating
Marathon-worthy (TV/anime)filter status === 'ended' AND 20 ≤ total_episodes ≤ 100, sort by rating
Mini-series (TV/anime)filter total_episodes ≤ 6
By genre / country / network / status / anime typeitems.filter(...) on the matching field
In theaters now (movies)filter theater date within last 30 days
Just released on DVD (movies)filter dvd_date within last 14-30 days
Just premiered (TV/anime/movies)filter status === 'premiere' — flags items whose first episode JUST aired (typically within ~30 days). Not an “upcoming premieres” filter; the data has no field for that.
By year (e.g. Movies of 2026)parse the 4-digit year out of release_date (any format), filter equality
By decade (e.g. Best of the 90s)parse year, filter decade ≤ y < decade + 10
Current year / last yearsame as By year, comparing against new Date().getFullYear() (or your language’s equivalent) — re-evaluated per call so the row stays current without re-shipping the JSON
Best of decadecombine By decade with bestOfNetwork’s shape — rating-sort + votes >= 100 floor inside the decade window
Has traileritems.filter(i => i.trailer) — for trailer-first carousel UIs
Hidden Gemsfilter rank > 2000 AND rating >= 7.5 AND votes >= 500, then sort by rating desc — well-loved titles OUTSIDE Simkl’s all-time top 2000 by popularity. Don’t filter by watched on trending feeds: that field is last-24h viewers, not lifetime, so a 150-cap surfaces mainstream titles like The Office or Interstellar. Rank is the right popularity gate; mainstream lives in rank 50-500, gems live in 2000+. Tune minRank up (3000, 5000) for stricter obscurity.
Critically acclaimed, widely seenrating >= 8.5 AND votes >= 1000 — high bar on both axes
Massive anticipationplan_to_watch / (watched + 1) >= 300 — pre-release / newly-announced (median ratio is ~200; 300 is upper quartile)
Cross-platform consensusSimkl rating ≥ 8 AND IMDb (or MAL) rating ≥ 8 — both crowds agree
Best of filter network match + status === 'ended', sort by rating
Grouped by genre / country / network / decadebucket items into { "Action": [...], "Drama": [...] } for N rows from one file
Tuning a recipe per catalog row. Every recipe accepts an options object as its second arg. In the JSON, set them via recipe_opts. For example, to make tv_hidden_gems stricter — only titles outside Simkl’s top 3000 with rating ≥ 8 instead of the default 2000/7.5 — override the knobs:
{
  "id": "tv_hidden_gems",
  "source": "tv",
  "title": "Hidden Gem TV on Simkl",
  "recipe": "hiddenGems",
  "recipe_opts": { "minRank": 3000, "minRating": 8 }
}
Same pattern works for any recipe — bestOfDecade with { "decade": 1980 }, bestOfNetwork with { "network": "AMC" }, highestRated with { "minVotes": 2000 }, etc. Check today’s live data before bumping thresholds aggressively though; over-strict opts can produce empty rows on smaller trending feeds.
Attribution still applies to derived rows. A row labeled “Most Watchlisted Movies” or “Hidden Gems” still needs to include “Simkl” or “Most Watched” to satisfy the attribution requirement — e.g. “Most Watchlisted on Simkl”, “Simkl Hidden Gems”, “Trending in Japan — Powered by Simkl”.

What’s in each item

Per-title arrays (movies, tv, anime) all share the same base shape, with a few type-specific fields.
Sample item
{
  "title": "The Drama",
  "url": "/movie/2533163/the-drama",
  "poster": "19/19702715dfee84bd7c",
  "fanart": "19/19840547ea18093f08",
  "ids": {
    "simkl_id": 2533163,
    "slug": "the-drama",
    "imdb": "tt33071426",
    "tmdb": "1148540"
  },
  "release_date": "03/26/2026",
  "rank": 3311,
  "drop_rate": "1.6%",
  "watched": 96,
  "plan_to_watch": 820,
  "ratings": {
    "simkl": { "rating": 7.37, "votes": 136 },
    "imdb":  { "rating": 7.4,  "votes": 42928 }
  },
  "country": "us",
  "runtime": "1h 45m",
  "status": "ended",
  "genres": ["Action", "Comedy", "Drama", "Romance"],
  "trailer": "0ZDzsH3XGFA",
  "overview": "A happily engaged couple is put to the test...",
  "metadata": "March 26, 2026 • Budget $28M • Box office $121M",
  "dvd_date": "05/05/2026",
  "theater": "03/26/2026"
}

Field reference

title
string
Display title.
url
string
Path on simkl.com (e.g. /movie/2533163/the-drama). Prepend https://simkl.com to deep-link.
poster
string
Image path fragment. Combine with the prefixes in Image conventions — for example https://wsrv.nl/?url=https://simkl.in/posters/{poster}_m.webp&q=90. When null, fall back to https://simkl.in/poster_no_pic.png (see fallbacks).
fanart
string
Image path fragment for fanart. Same prefixing rules as poster. When null, hide the fanart element — there’s no dedicated fanart placeholder.
ids
object
External and Simkl IDs. Always carries simkl_id + slug. tmdb is near-universal; imdb appears on TV / movies, mal / anidb / anilist / kitsu on anime. Additional third-party slug variants (letterslug, traktmslug, tvdbslug, trakttvslug, mdlslug, …) may appear on items with those platform links — the response is permissive.
rank
integer
Simkl service rank for this media type.
watched
integer
Number of users who watched this title in the timeframe.
plan_to_watch
integer
Number of users with this title on their Plan-to-Watch watchlist.
drop_rate
string
Percentage of users who started and dropped, formatted as a string (e.g. "1.6%").
ratings
object
Aggregate ratings — Simkl and any external sources available for this title (IMDB for movies/TV, MAL for anime, etc.). Each entry is { rating: number, votes: number }.
release_date
string
Original release date in MM/DD/YYYY format.
country
string
ISO-style country code (e.g. us, jp).
runtime
string
Human-readable duration (e.g. 1h 45m, 25m).
status
string
One of ended (production wrapped), ongoing (TV/anime still airing new episodes), or premiere (very-recently-released, still in the rollout window). The wider Simkl catalog also uses tba for not-yet-dated titles, but those don’t surface in trending.
genres
string[]
Array of genre tags.
trailer
string
YouTube video ID.
overview
string
Synopsis.
metadata
string
Pre-formatted human-readable summary line (release year, budget, box office, network, etc.).
dvd_date
string
Movies only. DVD release date (MM/DD/YYYY).
theater
string
Movies only. Theatrical release date (MM/DD/YYYY).
anime_type
string
Anime only. One of tv, movie, special, ova, ona, music video.
total_episodes
integer
TV / anime. Episode count.
network
string
TV / anime. Broadcasting network.

Without the SDK — raw URL and curl

The SimklTrendingClient above is the recommended way to consume trending data — it handles caching, conditional revalidation, retry, dedup, persistence, and the auto-pick combined endpoint logic. The patterns below are the bare-bones alternative for ad-hoc curl probes, server-side cron jobs that handle their own caching, or runtimes where you don’t want to drop a class file.

URL pattern

If you’d rather construct the URL in code:
https://data.simkl.in/discover/trending/[<category>/]<timeframe>_<size>.json
  • <category> — omit for Combined (movies + tv + anime in one response); use movies, tv, or anime for type-specific.
  • <timeframe>today, week, or month.
  • <size>100 or 500.
DVD releases use a different prefix and no timeframe:
https://data.simkl.in/discover/dvd/releases_<size>.json

Fetch it

curl -H 'User-Agent: my-app-name/1.0' \
  "https://data.simkl.in/discover/trending/today_100.json?client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0"
Always send a User-Agent with your app name and version (e.g. myapp/1.0) to avoid accidental blocking.