# StreamRank API

StreamRank publishes a daily list of titles that are heating up and are not on a Top 10 yet, with a measured hit rate against the next two weeks of charts. The same API serves those Top 10s for Netflix, Prime Video, Disney+, HBO Max, Hulu, Apple TV, Paramount+, Peacock, Starz, Canal+, Wavve, Coupang Play, and Claro Video.

Requests without a key run in the sample scope: country=US; provider=netflix; current charts only. Send `Authorization: Bearer sk_live_…` for your plan. A free account's live key reads every country and provider with 7 days of history; show a visible link to StreamRank wherever you display its data. Test keys (`sk_test_…`) stay in the sample scope and never count toward quota.

This is a daily pick list plus Top 10 charts, not a catalog of everything on a service. Picks and charts update once a day. Cache them.

Identifiers: provider ids are lowercase (`netflix`, not `Netflix`). Countries are ISO 3166-1 alpha-2 (`US`, not `USA`). Look up a title with `GET /v1/titles?q=` or `imdb_id` / `tmdb_id`. Title ids look like `ttl_…`.

- Base URL: https://streamrank.io/v1
- OpenAPI: https://streamrank.io/v1/openapi.json
- HTML docs: https://streamrank.io/docs
- This file: https://streamrank.io/docs.md

## Quickstart

Today's breakout picks in the US. No key required in the sample scope.

```bash
curl "https://streamrank.io/v1/trending/breakouts?country=US"
```

```json
{
  "data": {
    "scope": "US",
    "date": "2026-09-15",
    "items": [
      {
        "position": 1,
        "score": 45.4,
        "score_change": 5.2,
        "status": "rising",
        "signals": { "chart_momentum": 0, "demand": 83.1, "attention": 45.2 },
        "why": [
          "Lands Sep 19 on Netflix.",
          "Demand 83 of 100.",
          "Score up 5.2 today.",
          "On no Top 10 in United States."
        ],
        "strength": "strong",
        "premiere": "2026-09-19",
        "providers": ["netflix"],
        "outcome": {
          "first_chart_date": "2026-09-20",
          "first_chart_provider": "netflix",
          "first_chart_country": "US",
          "first_chart_rank": 4,
          "days_to_chart": 5,
          "peak_rank": 2,
          "days_on_chart": 6
        },
        "missed": false,
        "title": {
          "id": "ttl_9q2h7v3zkm1d",
          "slug": "obsession-2026",
          "name": "Obsession",
          "type": "movie",
          "year": 2026,
          "tmdb_id": 1364532,
          "imdb_id": "tt31520893",
          "poster_url": "https://image.tmdb.org/t/p/w500/obsession.jpg"
        }
      }
    ],
    "track_record": {
      "scope": "US",
      "from": "2026-08-02",
      "to": "2026-08-31",
      "window_days": 14,
      "long_window_days": 30,
      "types": ["tv"],
      "picks": 96,
      "charted_7d": 29,
      "charted_14d": 41,
      "hit_rate_14d": 42.7,
      "median_days_to_chart": 4,
      "median_peak_rank": 3,
      "median_days_on_chart": 7,
      "picks_30d": 45,
      "charted_30d": 21,
      "hit_rate_30d": 46.7,
      "pool": 640,
      "pool_charted_14d": 122,
      "pool_hit_rate_14d": 19.1,
      "pool_30d": 300,
      "pool_charted_30d": 66,
      "pool_hit_rate_30d": 22,
      "by_type": {
        "tv": {
          "picks": 96,
          "charted_7d": 29,
          "charted_14d": 41,
          "hit_rate_14d": 42.7,
          "median_days_to_chart": 4,
          "median_peak_rank": 3,
          "median_days_on_chart": 7,
          "picks_30d": 45,
          "charted_30d": 21,
          "hit_rate_30d": 46.7,
          "pool": 640,
          "pool_charted_14d": 122,
          "pool_hit_rate_14d": 19.1,
          "pool_30d": 300,
          "pool_charted_30d": 66,
          "pool_hit_rate_30d": 22,
          "control": {
            "picks": 150,
            "charted_7d": 9,
            "charted_14d": 16,
            "hit_rate_14d": 10.7,
            "median_days_to_chart": 7,
            "median_peak_rank": 7,
            "median_days_on_chart": 2,
            "picks_30d": 70,
            "charted_30d": 11,
            "hit_rate_30d": 15.7,
            "pool": 640,
            "pool_charted_14d": 122,
            "pool_hit_rate_14d": 19.1,
            "pool_30d": 300,
            "pool_charted_30d": 66,
            "pool_hit_rate_30d": 22
          }
        },
        "movie": {
          "picks": 104,
          "charted_7d": 6,
          "charted_14d": 9,
          "hit_rate_14d": 8.7,
          "median_days_to_chart": 9,
          "median_peak_rank": 6,
          "median_days_on_chart": 3,
          "picks_30d": 50,
          "charted_30d": 8,
          "hit_rate_30d": 16,
          "pool": 1570,
          "pool_charted_14d": 76,
          "pool_hit_rate_14d": 4.8,
          "pool_30d": 730,
          "pool_charted_30d": 88,
          "pool_hit_rate_30d": 12.1,
          "control": {
            "picks": 150,
            "charted_7d": 6,
            "charted_14d": 11,
            "hit_rate_14d": 7.3,
            "median_days_to_chart": 8,
            "median_peak_rank": 8,
            "median_days_on_chart": 2,
            "picks_30d": 70,
            "charted_30d": 9,
            "hit_rate_30d": 12.9,
            "pool": 1570,
            "pool_charted_14d": 76,
            "pool_hit_rate_14d": 4.8,
            "pool_30d": 730,
            "pool_charted_30d": 88,
            "pool_hit_rate_30d": 12.1
          }
        }
      },
      "by_strength": {
        "strong": {
          "picks": 30,
          "charted_7d": 12,
          "charted_14d": 16,
          "hit_rate_14d": 53.3,
          "median_days_to_chart": 3,
          "median_peak_rank": 2,
          "median_days_on_chart": 9,
          "picks_30d": 14,
          "charted_30d": 8,
          "hit_rate_30d": 57.1,
          "pool": 640,
          "pool_charted_14d": 122,
          "pool_hit_rate_14d": 19.1,
          "pool_30d": 300,
          "pool_charted_30d": 66,
          "pool_hit_rate_30d": 22
        },
        "watch": {
          "picks": 66,
          "charted_7d": 17,
          "charted_14d": 25,
          "hit_rate_14d": 37.9,
          "median_days_to_chart": 5,
          "median_peak_rank": 4,
          "median_days_on_chart": 6,
          "picks_30d": 31,
          "charted_30d": 13,
          "hit_rate_30d": 41.9,
          "pool": 640,
          "pool_charted_14d": 122,
          "pool_hit_rate_14d": 19.1,
          "pool_30d": 300,
          "pool_charted_30d": 66,
          "pool_hit_rate_30d": 22
        }
      },
      "control": {
        "picks": 150,
        "charted_7d": 9,
        "charted_14d": 16,
        "hit_rate_14d": 10.7,
        "median_days_to_chart": 7,
        "median_peak_rank": 7,
        "median_days_on_chart": 2,
        "picks_30d": 70,
        "charted_30d": 11,
        "hit_rate_30d": 15.7,
        "pool": 640,
        "pool_charted_14d": 122,
        "pool_hit_rate_14d": 19.1,
        "pool_30d": 300,
        "pool_charted_30d": 66,
        "pool_hit_rate_30d": 22
      }
    }
  },
  "meta": { "count": 1, "locked": 0, "limit_max": 10, "history_days": 30 }
}
```

Picks and charts update once a day. Cache them. Top 10s are at `GET /v1/charts`:

```bash
curl "https://streamrank.io/v1/charts?provider=netflix&country=US&type=movie" \
  -H "Authorization: Bearer sk_live_…"
```

## Authentication

Send your key in the `Authorization` header. Live keys start with `sk_live_` and read your plan; on Free they stay in the sample scope, and anything outside it returns `403 plan_required` with an `upgrade_url`. Test keys start with `sk_test_`, return real data in the sample scope, and never count toward quota.

```http
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxx
```

Requests without a key work in the sample scope (United States; Netflix; current charts) at 10 requests per minute and 200 per calendar month per IP.

| Plan | Countries | Providers | History | Trending list | Requests / mo | Alerts |
| --- | --- | --- | --- | --- | --- | --- |
| Free | All | All | 7 days | Top 10 | Sample scope only | None |
| Pro | All | All | Full | Top 100 | 200,000 | 200 titles + markets, email, webhook, slack |

Pro starts with a 7-day free trial; a card is required and you can cancel before it ends.

## Breakouts

<a id="breakouts"></a>

`GET /v1/trending/breakouts`

The daily breakout picks for a scope: titles on zero Top 10 charts inside the scope, on the list for more than a day, and off every chart for the previous 30 days (under any title of the same name), that reached demand 40 out of 100, or attention 50 with demand 20, or have a premiere, season start, or digital release inside the scope within 3 days with demand 20 or attention 40, or at any level when a tracked service in the scope is the title's home. An episode that airs within 21 days of the one before it continues a run and is not a release. Ten shows a day. A title with a known release more than 3 days out waits until the release is inside 3 days. Shows rank launches first (a release inside 3 days or in the 14 days before the pick), then originals on their own service, then titles landing within 3 days, then demand level. Inside each of those groups, fading titles go last, so a fading launch still ranks above a steady library show. Each item carries `strength`: `strong` for a launch, `watch` for the rest. A show picked five days running with no chart rests until something changes. Five movie picks a day are logged the same way, but only movie launches publish: a digital release or premiere inside 3 days on a tracked service the movie is made for. They rank first among movies and are returned with `type=movie`. Other movie picks charted at the rate of a random draw from the pool and stay in the ledger. On the worldwide scope, charts in Japan and South Korea do not count: they neither block a pick nor count as its outcome. Each item carries `why`, plain-language reasons built from the categories and release data, and `outcome` once the title charts. `track_record` counts calls, not pick days: a title picked on consecutive days (gaps up to 7 days) is one call dated from its first day. Top-level numbers cover the published types (`types`); `by_type` holds every type with its own `control`, and `by_strength` splits the published calls into strong and watch.

| Name | Type | Description |
| --- | --- | --- |
| `country` | string | Optional. ISO 3166-1 alpha-2. Omit it for the global list. A country scope means zero Top 10 charts in that country. |
| `type` | movie \| tv | Optional. Default `tv`, the published list. `movie` returns the movie picks, which are logged every day but not published while they chart at the control rate. |
| `limit` | integer | Optional. 1–50, default 10, capped by your plan's trending limit. On a list younger than 14 days, Free, test keys, and calls without a key get the top pick and the rest is reported in `meta.locked`; paid plans get all of them. Lists 14 days old or older return in full for everyone. |
| `date` | YYYY-MM-DD | Optional. The newest pick list on or before this date, inside your plan's history window. |

```http
GET /v1/trending/breakouts
GET /v1/trending/breakouts?country=US&type=tv
GET /v1/trending/breakouts?country=GB&date=2026-09-10
```

Fields of `data`:

| Field | Type | Description |
| --- | --- | --- |
| `scope` | string | Country code, or `global`. |
| `date` | date | The pick list day. |
| `items` | array | The day's picks, in list order. |
| `items[].position` | integer | Place on the day's list, 1 first. |
| `items[].score` | number | Trend score, 0–100, in this scope. |
| `items[].score_change` | number \| null | Change since yesterday. |
| `items[].status` | new \| rising \| steady \| peaking \| fading | `new`: on this list today and not in the 7 days before. `rising`: the signals are growing against their own 7-day average and the title has already reached a meaningful level (a single new chart entry does not qualify). `steady`: little day-over-day change. `peaking`: near the top of the scale and no longer growing. `fading`: the signals are shrinking. |
| `items[].signals` | object | Category levels, each 0–100, or null when that category had no data that day. |
| `items[].signals.chart_momentum` | number \| null | Position, breadth, and movement across Top 10 charts inside the scope. |
| `items[].signals.demand` | number \| null | Consumption and discovery intent. First-party activity is worldwide and counted over the last complete day; discovery rankings follow the country scope and are summed on the global list. |
| `items[].signals.attention` | number \| null | Broad web attention, from one worldwide feed and from reading per language that follows the country (summed across languages on the global list). Discounted when flat: a title that draws the same attention every day keeps half its level, a spike keeps all of it. Attention alone never carries a title above titles with chart or demand presence unless it is spiking. |
| `items[].why` | array | Plain-language reasons, built from the categories and release data. |
| `items[].strength` | strong \| watch | `strong` for a launch (a release inside 3 days or in the 14 days before the pick), `watch` for the rest. |
| `items[].premiere` | date \| null | Next digital release, premiere, or season start inside the scope, known on pick day. |
| `items[].providers` | array | Tracked services carrying the title inside the scope, known on pick day. |
| `items[].outcome` | object \| null | Set once the title enters a Top 10 inside the scope within 30 days. Null while the window is open or after it closed without a chart. |
| `items[].outcome.first_chart_date` | date | First day on a Top 10 inside the scope. |
| `items[].outcome.first_chart_provider` | string | Provider of that first chart. |
| `items[].outcome.first_chart_country` | string | Country of that first chart. |
| `items[].outcome.first_chart_rank` | integer | Rank on that first chart. |
| `items[].outcome.days_to_chart` | integer | Days from pick to first chart. |
| `items[].outcome.peak_rank` | integer | Best rank inside the scope, updated until day 30. |
| `items[].outcome.days_on_chart` | integer | Distinct chart days inside the scope, updated until day 30. |
| `items[].missed` | boolean | True once 14 days pass without a chart. `outcome` can still fill in later, up to day 30. |
| `items[].title` | object | Canonical title: id, slug, name, type, year, tmdb_id, imdb_id, poster_url. |
| `track_record` | object | Hit rate of past calls next to the rate for every eligible title. |
| `track_record.scope` | string | Country code, or `global`. |
| `track_record.from` | date | First pick day counted. |
| `track_record.to` | date | Last pick day counted. |
| `track_record.window_days` | integer | The main window, 14 days. |
| `track_record.long_window_days` | integer | The long window, 30 days. |
| `track_record.types` | array | Content types the top-level numbers cover: the published types. |
| `track_record.picks` | integer | Calls whose 14-day window has closed. A title picked on consecutive days (gaps up to 7 days) is one call, dated from its first day. |
| `track_record.charted_7d` | integer | Calls that entered a Top 10 inside the scope within 7 days. |
| `track_record.charted_14d` | integer | Calls that entered a Top 10 inside the scope within 14 days. |
| `track_record.hit_rate_14d` | number \| null | charted_14d as a percentage of picks. |
| `track_record.median_days_to_chart` | number \| null | Median days from pick to first chart, over the calls that charted within 14 days. |
| `track_record.median_peak_rank` | number \| null | Median best rank inside the scope, over 14-day hits whose 30-day window has closed. |
| `track_record.median_days_on_chart` | number \| null | Median chart days inside the scope, over the same hits. |
| `track_record.picks_30d` | integer | Calls whose 30-day window has closed. |
| `track_record.charted_30d` | integer | Of those, the calls that charted within 30 days. |
| `track_record.hit_rate_30d` | number \| null | charted_30d as a percentage of picks_30d. |
| `track_record.pool` | integer | Every uncharted title that met the rules on the same days. |
| `track_record.pool_charted_14d` | integer | Pool titles that charted within 14 days. |
| `track_record.pool_hit_rate_14d` | number \| null | The pool's 14-day rate. The list has to beat it to mean anything. |
| `track_record.pool_30d` | integer | Pool titles on the days whose 30-day window has closed. |
| `track_record.pool_charted_30d` | integer | Of those, the titles that charted within 30 days. |
| `track_record.pool_hit_rate_30d` | number \| null | The pool's 30-day rate. |
| `track_record.control` | object | Ten random titles a day from the same eligible pool, checked the same way as picks. The same fields as the top level. The fairest baseline. |
| `track_record.by_type` | object | `tv` and `movie`, each with the same fields and its own `control`. Movies are logged but not published; their record lives here. |
| `track_record.by_strength` | object | `strong` and `watch`: the published calls split by strength, with the same fields. Pool fields repeat the whole pool. |

Fields of `meta`:

| Field | Type | Description |
| --- | --- | --- |
| `count` | integer | Items returned. |
| `limit_max` | integer | Largest `limit` your plan allows. |
| `history_days` | integer | Days of history your plan includes. |
| `locked` | integer | Picks your plan holds back on a list younger than 14 days. |


```json
{
  "data": {
    "scope": "US",
    "date": "2026-09-15",
    "items": [
      {
        "position": 1,
        "score": 45.4,
        "score_change": 5.2,
        "status": "rising",
        "signals": { "chart_momentum": 0, "demand": 83.1, "attention": 45.2 },
        "why": [
          "Lands Sep 19 on Netflix.",
          "Demand 83 of 100.",
          "Score up 5.2 today.",
          "On no Top 10 in United States."
        ],
        "strength": "strong",
        "premiere": "2026-09-19",
        "providers": ["netflix"],
        "outcome": {
          "first_chart_date": "2026-09-20",
          "first_chart_provider": "netflix",
          "first_chart_country": "US",
          "first_chart_rank": 4,
          "days_to_chart": 5,
          "peak_rank": 2,
          "days_on_chart": 6
        },
        "missed": false,
        "title": {
          "id": "ttl_9q2h7v3zkm1d",
          "slug": "obsession-2026",
          "name": "Obsession",
          "type": "movie",
          "year": 2026,
          "tmdb_id": 1364532,
          "imdb_id": "tt31520893",
          "poster_url": "https://image.tmdb.org/t/p/w500/obsession.jpg"
        }
      }
    ],
    "track_record": {
      "scope": "US",
      "from": "2026-08-02",
      "to": "2026-08-31",
      "window_days": 14,
      "long_window_days": 30,
      "types": ["tv"],
      "picks": 96,
      "charted_7d": 29,
      "charted_14d": 41,
      "hit_rate_14d": 42.7,
      "median_days_to_chart": 4,
      "median_peak_rank": 3,
      "median_days_on_chart": 7,
      "picks_30d": 45,
      "charted_30d": 21,
      "hit_rate_30d": 46.7,
      "pool": 640,
      "pool_charted_14d": 122,
      "pool_hit_rate_14d": 19.1,
      "pool_30d": 300,
      "pool_charted_30d": 66,
      "pool_hit_rate_30d": 22,
      "by_type": {
        "tv": {
          "picks": 96,
          "charted_7d": 29,
          "charted_14d": 41,
          "hit_rate_14d": 42.7,
          "median_days_to_chart": 4,
          "median_peak_rank": 3,
          "median_days_on_chart": 7,
          "picks_30d": 45,
          "charted_30d": 21,
          "hit_rate_30d": 46.7,
          "pool": 640,
          "pool_charted_14d": 122,
          "pool_hit_rate_14d": 19.1,
          "pool_30d": 300,
          "pool_charted_30d": 66,
          "pool_hit_rate_30d": 22,
          "control": {
            "picks": 150,
            "charted_7d": 9,
            "charted_14d": 16,
            "hit_rate_14d": 10.7,
            "median_days_to_chart": 7,
            "median_peak_rank": 7,
            "median_days_on_chart": 2,
            "picks_30d": 70,
            "charted_30d": 11,
            "hit_rate_30d": 15.7,
            "pool": 640,
            "pool_charted_14d": 122,
            "pool_hit_rate_14d": 19.1,
            "pool_30d": 300,
            "pool_charted_30d": 66,
            "pool_hit_rate_30d": 22
          }
        },
        "movie": {
          "picks": 104,
          "charted_7d": 6,
          "charted_14d": 9,
          "hit_rate_14d": 8.7,
          "median_days_to_chart": 9,
          "median_peak_rank": 6,
          "median_days_on_chart": 3,
          "picks_30d": 50,
          "charted_30d": 8,
          "hit_rate_30d": 16,
          "pool": 1570,
          "pool_charted_14d": 76,
          "pool_hit_rate_14d": 4.8,
          "pool_30d": 730,
          "pool_charted_30d": 88,
          "pool_hit_rate_30d": 12.1,
          "control": {
            "picks": 150,
            "charted_7d": 6,
            "charted_14d": 11,
            "hit_rate_14d": 7.3,
            "median_days_to_chart": 8,
            "median_peak_rank": 8,
            "median_days_on_chart": 2,
            "picks_30d": 70,
            "charted_30d": 9,
            "hit_rate_30d": 12.9,
            "pool": 1570,
            "pool_charted_14d": 76,
            "pool_hit_rate_14d": 4.8,
            "pool_30d": 730,
            "pool_charted_30d": 88,
            "pool_hit_rate_30d": 12.1
          }
        }
      },
      "by_strength": {
        "strong": {
          "picks": 30,
          "charted_7d": 12,
          "charted_14d": 16,
          "hit_rate_14d": 53.3,
          "median_days_to_chart": 3,
          "median_peak_rank": 2,
          "median_days_on_chart": 9,
          "picks_30d": 14,
          "charted_30d": 8,
          "hit_rate_30d": 57.1,
          "pool": 640,
          "pool_charted_14d": 122,
          "pool_hit_rate_14d": 19.1,
          "pool_30d": 300,
          "pool_charted_30d": 66,
          "pool_hit_rate_30d": 22
        },
        "watch": {
          "picks": 66,
          "charted_7d": 17,
          "charted_14d": 25,
          "hit_rate_14d": 37.9,
          "median_days_to_chart": 5,
          "median_peak_rank": 4,
          "median_days_on_chart": 6,
          "picks_30d": 31,
          "charted_30d": 13,
          "hit_rate_30d": 41.9,
          "pool": 640,
          "pool_charted_14d": 122,
          "pool_hit_rate_14d": 19.1,
          "pool_30d": 300,
          "pool_charted_30d": 66,
          "pool_hit_rate_30d": 22
        }
      },
      "control": {
        "picks": 150,
        "charted_7d": 9,
        "charted_14d": 16,
        "hit_rate_14d": 10.7,
        "median_days_to_chart": 7,
        "median_peak_rank": 7,
        "median_days_on_chart": 2,
        "picks_30d": 70,
        "charted_30d": 11,
        "hit_rate_30d": 15.7,
        "pool": 640,
        "pool_charted_14d": 122,
        "pool_hit_rate_14d": 19.1,
        "pool_30d": 300,
        "pool_charted_30d": 66,
        "pool_hit_rate_30d": 22
      }
    }
  },
  "meta": { "count": 1, "locked": 0, "limit_max": 10, "history_days": 30 }
}
```

A pick is logged the day it is made. `outcome` is filled in when the title enters a Top 10 inside the scope within 30 days; `missed` turns true when 14 days pass without one. `outcome.peak_rank` and `outcome.days_on_chart` are the best rank and the distinct chart days inside the scope, updated until day 30. `track_record` only counts calls whose 14-day window has closed; a title picked on consecutive days (gaps up to 7 days) is one call dated from its first day. Top-level numbers cover the published types (`types`, shows only for now); `by_type` has every type with its own `control`. Movie picks are logged but not published, and come back with `type=movie`. `median_peak_rank` and `median_days_on_chart` describe the hits whose 30-day window has closed, so a list that grazes #10 for a day reads differently from one that holds #1. The `*_30d` fields cover the older calls whose 30-day window has closed too. `pool_hit_rate_14d` is the same measure for every uncharted title that met the rules on the same days. The list has to beat the pool to mean anything.

### Pick history

<a id="breakouts-history"></a>

`GET /v1/trending/breakouts/history`

Past picks for a scope, newest day first and in list order inside a day. Shows by default; `type=movie` returns the published movie launches. Each item is a pick row with the day it was made, the reasons we gave, and the outcome we recorded: `outcome` once the title entered a Top 10 inside the scope within 30 days, `missed` once 14 days passed without one. `from` is clamped to your plan's history window instead of failing. Page with `limit` and `offset`; `meta.total` is the full count. On a list younger than 14 days, Free, test keys, and calls without a key get the top pick only.

| Name | Type | Description |
| --- | --- | --- |
| `country` | string | Optional. ISO 3166-1 alpha-2. Omit it for the global list. |
| `type` | movie \| tv | Optional. Default `tv`, the published list. `movie` returns the movie picks, which are logged every day but not published while they chart at the control rate. |
| `from` | YYYY-MM-DD | Optional start date. Clamped to your plan's history window. |
| `to` | YYYY-MM-DD | Optional end date. |
| `limit` | 1–500 | Optional. Picks per page, default 100. |
| `offset` | integer | Optional. Picks to skip, default 0. Page with `meta.total`. |

```http
GET /v1/trending/breakouts/history?country=US
GET /v1/trending/breakouts/history?country=US&type=tv&from=2026-08-01&to=2026-08-31
GET /v1/trending/breakouts/history?limit=50&offset=50
```

Fields of `data`:

| Field | Type | Description |
| --- | --- | --- |
| `scope` | string | Country code, or `global`. |
| `from` | date | First pick day, after clamping to your plan's history window. |
| `to` | date | Last pick day. |
| `items` | array | Picks, newest day first and in list order inside a day. |
| `items[].date` | date | Day the pick was made. |
| `items[].position` | integer | Place on the day's list, 1 first. |
| `items[].score` | number | Trend score, 0–100, in this scope. |
| `items[].score_change` | number \| null | Change since yesterday. |
| `items[].status` | new \| rising \| steady \| peaking \| fading | `new`: on this list today and not in the 7 days before. `rising`: the signals are growing against their own 7-day average and the title has already reached a meaningful level (a single new chart entry does not qualify). `steady`: little day-over-day change. `peaking`: near the top of the scale and no longer growing. `fading`: the signals are shrinking. |
| `items[].signals` | object | Category levels, each 0–100, or null when that category had no data that day. |
| `items[].signals.chart_momentum` | number \| null | Position, breadth, and movement across Top 10 charts inside the scope. |
| `items[].signals.demand` | number \| null | Consumption and discovery intent. First-party activity is worldwide and counted over the last complete day; discovery rankings follow the country scope and are summed on the global list. |
| `items[].signals.attention` | number \| null | Broad web attention, from one worldwide feed and from reading per language that follows the country (summed across languages on the global list). Discounted when flat: a title that draws the same attention every day keeps half its level, a spike keeps all of it. Attention alone never carries a title above titles with chart or demand presence unless it is spiking. |
| `items[].why` | array | Plain-language reasons, built from the categories and release data. |
| `items[].strength` | strong \| watch | `strong` for a launch (a release inside 3 days or in the 14 days before the pick), `watch` for the rest. |
| `items[].premiere` | date \| null | Next digital release, premiere, or season start inside the scope, known on pick day. |
| `items[].providers` | array | Tracked services carrying the title inside the scope, known on pick day. |
| `items[].outcome` | object \| null | Set once the title enters a Top 10 inside the scope within 30 days. Null while the window is open or after it closed without a chart. |
| `items[].outcome.first_chart_date` | date | First day on a Top 10 inside the scope. |
| `items[].outcome.first_chart_provider` | string | Provider of that first chart. |
| `items[].outcome.first_chart_country` | string | Country of that first chart. |
| `items[].outcome.first_chart_rank` | integer | Rank on that first chart. |
| `items[].outcome.days_to_chart` | integer | Days from pick to first chart. |
| `items[].outcome.peak_rank` | integer | Best rank inside the scope, updated until day 30. |
| `items[].outcome.days_on_chart` | integer | Distinct chart days inside the scope, updated until day 30. |
| `items[].missed` | boolean | True once 14 days pass without a chart. `outcome` can still fill in later, up to day 30. |
| `items[].title` | object | Canonical title: id, slug, name, type, year, tmdb_id, imdb_id, poster_url. |

Fields of `meta`:

| Field | Type | Description |
| --- | --- | --- |
| `count` | integer | Items returned. |
| `total` | integer | Every pick that matches the query. Page against it. |
| `limit` | integer | Page size. |
| `offset` | integer | Picks skipped. |
| `history_days` | integer | Days of history your plan includes. |


```json
{
  "data": {
    "scope": "US",
    "from": "2026-08-16",
    "to": "2026-09-15",
    "items": [
      {
        "date": "2026-09-15",
        "position": 1,
        "score": 45.4,
        "score_change": 5.2,
        "status": "rising",
        "signals": { "chart_momentum": 0, "demand": 83.1, "attention": 45.2 },
        "why": [
          "Lands Sep 19 on Netflix.",
          "Demand 83 of 100.",
          "Score up 5.2 today.",
          "On no Top 10 in United States."
        ],
        "strength": "strong",
        "premiere": "2026-09-19",
        "providers": ["netflix"],
        "outcome": {
          "first_chart_date": "2026-09-20",
          "first_chart_provider": "netflix",
          "first_chart_country": "US",
          "first_chart_rank": 4,
          "days_to_chart": 5,
          "peak_rank": 2,
          "days_on_chart": 6
        },
        "missed": false,
        "title": {
          "id": "ttl_9q2h7v3zkm1d",
          "slug": "obsession-2026",
          "name": "Obsession",
          "type": "movie",
          "year": 2026,
          "tmdb_id": 1364532,
          "imdb_id": "tt31520893",
          "poster_url": "https://image.tmdb.org/t/p/w500/obsession.jpg"
        }
      }
    ]
  },
  "meta": {
    "count": 1,
    "total": 310,
    "limit": 100,
    "offset": 0,
    "history_days": 30
  }
}
```

Every pick we have made in the scope, newest day first, with the outcome we recorded for each. `from` is clamped to your plan's history window. Page with `limit` and `offset` against `meta.total`. On a list younger than 14 days, Free, test keys, and calls without a key get the top pick only.


## Charts

<a id="charts"></a>

`GET /v1/charts`

The Top 10 for one provider in one country. Omit `type` to receive both the movie and TV charts in one response.

| Name | Type | Description |
| --- | --- | --- |
| `provider` | string | Required. netflix, prime, disney, max, hulu, apple, paramount, peacock, starz, canal, wavve, coupang, claro. |
| `country` | string | Required. ISO 3166-1 alpha-2, e.g. US, GB, JP. |
| `type` | movie \| tv | Optional. Return one chart instead of both. |
| `date` | YYYY-MM-DD | Optional. A past chart. Resolves to the newest chart on or before the date. Requires history on your plan. |
| `limit` | 1–10 | Optional. Return only the top N. |

```http
GET /v1/charts?provider=netflix&country=US&type=movie
GET /v1/charts?provider=netflix&country=JP&type=tv
GET /v1/charts?provider=disney&country=GB
GET /v1/charts?provider=max&country=US&type=tv&date=2026-09-01
```

Fields of `data`:

| Field | Type | Description |
| --- | --- | --- |
| `provider` | string | Provider id. |
| `country` | string | ISO 3166-1 alpha-2. |
| `type` | movie \| tv | Which chart. Present when you pass `type`. |
| `date` | date | Chart day. |
| `updated_at` | datetime | When we last stored this chart. |
| `rankings` | array | The Top 10, best first. |
| `rankings[].rank` | integer | Position on the chart, 1–10. |
| `rankings[].previous_rank` | integer \| null | Rank the day before, or null if the title was not on yesterday's chart. |
| `rankings[].rank_change` | integer \| null | previous_rank − rank. Positive means it moved up. |
| `rankings[].is_new` | boolean | True on the first day a title appears on this chart. |
| `rankings[].days_in_top_10` | integer | Total days on this chart, including today. |
| `rankings[].peak_rank` | integer | Best rank so far on this chart. |
| `rankings[].first_charted_at` | date | First day on this chart. |
| `rankings[].title` | object | Canonical title: id, slug, name, type, year, tmdb_id, imdb_id, poster_url. |
| `charts` | array | Without `type`: one chart per type, each with provider, country, type, date, updated_at, and rankings. |


Derived metrics come from our stored history, not from the upstream source.

### Channel charts

<a id="channel-charts"></a>

`GET /v1/channel-charts`

What Prime Video members watch through one channel (Crunchyroll, Discovery+, AMC+, …) in one country. Movies and shows are ranked together. It is not the service's own Top 10, so it stays out of trending scores and breakouts. Needs a plan that includes every provider.

| Name | Type | Description |
| --- | --- | --- |
| `channel` | string | Required. crunchyroll, discovery, amc, mgm, shudder, mubi, lionsgate, hayu, canal. |
| `country` | string | Required. ISO 3166-1 alpha-2, e.g. US, GB, DE. |
| `date` | YYYY-MM-DD | Optional. A past chart. Resolves to the newest chart on or before the date. Requires history on your plan. |
| `limit` | 1–10 | Optional. Return only the top N. |

```http
GET /v1/channel-charts?channel=crunchyroll&country=US
GET /v1/channel-charts?channel=discovery&country=IT&limit=5
```

Fields of `data`:

| Field | Type | Description |
| --- | --- | --- |
| `channel` | string | Channel id. |
| `country` | string | ISO 3166-1 alpha-2. |
| `date` | date | Chart day. |
| `updated_at` | datetime | When we last stored this chart. |
| `rankings` | array | The Top 10, best first. Movies and shows together; `title.type` says which. |
| `rankings[].rank` | integer | Position on the chart, 1–10. |
| `rankings[].previous_rank` | integer \| null | Rank the day before, or null if the title was not on yesterday's chart. |
| `rankings[].rank_change` | integer \| null | previous_rank − rank. Positive means it moved up. |
| `rankings[].is_new` | boolean | True on the first day a title appears on this chart. |
| `rankings[].days_in_top_10` | integer | Total days on this chart, including today. |
| `rankings[].peak_rank` | integer | Best rank so far on this chart. |
| `rankings[].first_charted_at` | date | First day on this chart. |
| `rankings[].title` | object | Canonical title: id, slug, name, type, year, tmdb_id, imdb_id, poster_url. |


```json
{
  "data": {
    "channel": "crunchyroll",
    "country": "US",
    "date": "2026-10-23",
    "updated_at": "2026-10-23T14:27:28.970Z",
    "rankings": [
      {
        "rank": 1,
        "previous_rank": 7,
        "rank_change": 6,
        "is_new": false,
        "days_in_top_10": 7,
        "peak_rank": 1,
        "first_charted_at": "2026-10-17",
        "title": {
          "id": "ttl_3n8c1rkq0w2e",
          "slug": "a-returners-magic-should-be-special-2023",
          "name": "A Returner's Magic Should Be Special",
          "type": "tv",
          "year": 2023,
          "tmdb_id": 220542,
          "imdb_id": "tt27443183",
          "poster_url": "https://image.tmdb.org/t/p/w500/….jpg"
        }
      }
    ]
  }
}
```

A channel chart is one list: movies and shows ranked together, so read `title.type`. Coverage is in `GET /v1/channels`. Channel charts never feed trending scores or breakouts.

## Titles

<a id="titles"></a>
<a id="title-search"></a>

`GET /v1/titles`

Returns a ranked list. Ambiguous names are not a 404. Then call GET /v1/titles/{id} for current positions. At least one of q, imdb_id, or tmdb_id is required.

| Name | Type | Description |
| --- | --- | --- |
| `q` | string | Name, slug, ttl_ id, IMDb id, or TMDB id. Required unless imdb_id or tmdb_id is set. |
| `imdb_id` | string | Exact IMDb id, e.g. tt4574334. |
| `tmdb_id` | integer | Exact TMDB id. Pass type when a movie and a show share the number. |
| `type` | movie \| tv | Optional. Restrict results to one kind. |
| `year` | integer | Optional. Prefer titles released this year when matching by name. |
| `limit` | 1–20 | Optional. Default 10. |

```http
GET /v1/titles?q=The+Pitt
GET /v1/titles?imdb_id=tt4574334
GET /v1/titles?tmdb_id=1396&type=tv
GET /v1/titles?q=Ready+or+Not&year=2019
```

Each item in `data`:

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | Title id, `ttl_…`. |
| `slug` | string | URL slug, e.g. `the-whisper-man-2026`. |
| `name` | string | Display name. |
| `type` | movie \| tv | Movie or show. |
| `year` | integer \| null | Release year. |
| `tmdb_id` | integer \| null | TMDB id. |
| `imdb_id` | string \| null | IMDb id. |
| `poster_url` | string \| null | Poster image. |
| `match` | name \| slug \| id \| imdb_id \| tmdb_id | What the query matched. |

Fields of `meta`:

| Field | Type | Description |
| --- | --- | --- |
| `count` | integer | Results returned. |


```json
{
  "data": [
    {
      "id": "ttl_5k2m9x1qzc7v",
      "slug": "the-whisper-man-2026",
      "name": "The Whisper Man",
      "type": "movie",
      "year": 2026,
      "tmdb_id": 860508,
      "imdb_id": "tt11561116",
      "poster_url": "https://image.tmdb.org/t/p/w500/….jpg",
      "match": "name"
    }
  ],
  "meta": { "count": 1 }
}
```

<a id="title"></a>

`GET /v1/titles/{id}`

Accepts a title id (`ttl_…`), slug, IMDb id, or TMDB id. TMDB ids that match both a movie and a show should use GET /v1/titles?tmdb_id=&type= instead. Returns where it sits on every current chart plus lifetime stats.

| Name | Type | Description |
| --- | --- | --- |
| `id` | string | Title id (`ttl_…`), slug, IMDb id, or numeric TMDB id. |

```http
GET /v1/titles/ttl_5k2m9x1qzc7v
GET /v1/titles/the-whisper-man-2026
GET /v1/titles/tt11561116
```

Fields of `data`:

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | Title id, `ttl_…`. |
| `slug` | string | URL slug, e.g. `the-whisper-man-2026`. |
| `name` | string | Display name. |
| `type` | movie \| tv | Movie or show. |
| `year` | integer \| null | Release year. |
| `tmdb_id` | integer \| null | TMDB id. |
| `imdb_id` | string \| null | IMDb id. |
| `poster_url` | string \| null | Poster image. |
| `overview` | string \| null | Synopsis. |
| `backdrop_url` | string \| null | Backdrop image. |
| `current_rankings` | array | Every current chart the title is on, inside your plan. |
| `current_rankings[].date` | date | Chart day. |
| `current_rankings[].provider` | string | Provider id. |
| `current_rankings[].country` | string | ISO 3166-1 alpha-2. |
| `current_rankings[].type` | movie \| tv | Which chart. |
| `current_rankings[].rank` | integer | Position on that chart, 1–10. |
| `current_rankings[].previous_rank` | integer \| null | Rank the day before. |
| `current_rankings[].rank_change` | integer \| null | previous_rank − rank. Positive means it moved up. |
| `current_rankings[].days_in_top_10` | integer | Total days on this chart. |
| `current_rankings[].peak_rank` | integer | Best rank so far on this chart. |
| `stats` | object | Lifetime stats over the countries, providers, and history your plan includes. |
| `stats.total_top_10_days` | integer | Chart days across every chart. |
| `stats.peak_rank` | integer \| null | Best rank on any chart. |
| `stats.days_at_number_one` | integer | Chart days at #1. |
| `stats.countries_charted` | integer | Countries where it has charted. |
| `stats.first_charted_at` | date \| null | First chart day. |
| `stats.last_charted_at` | date \| null | Most recent chart day. |


```json
{
  "data": {
    "id": "ttl_5k2m9x1qzc7v",
    "slug": "the-whisper-man-2026",
    "name": "The Whisper Man",
    "type": "movie",
    "year": 2026,
    "tmdb_id": 860508,
    "imdb_id": "tt11561116",
    "poster_url": "https://image.tmdb.org/t/p/w500/….jpg",
    "overview": "…",
    "backdrop_url": "https://image.tmdb.org/t/p/original/….jpg",
    "current_rankings": [
      {
        "date": "2026-09-10",
        "provider": "netflix",
        "country": "US",
        "type": "movie",
        "rank": 1,
        "previous_rank": 3,
        "rank_change": 2,
        "days_in_top_10": 8,
        "peak_rank": 1
      }
    ],
    "stats": {
      "total_top_10_days": 8,
      "peak_rank": 1,
      "days_at_number_one": 3,
      "countries_charted": 1,
      "first_charted_at": "2026-09-03",
      "last_charted_at": "2026-09-10"
    }
  }
}
```

<a id="title-rankings"></a>

`GET /v1/titles/{id}/rankings`

Newest first, clamped to your plan's history window.

| Name | Type | Description |
| --- | --- | --- |
| `id` | string | Title id (`ttl_…`) or slug. |
| `provider` | string | Optional filter. |
| `country` | string | Optional filter. |
| `from` | YYYY-MM-DD | Optional start date. |
| `to` | YYYY-MM-DD | Optional end date. |
| `limit` | 1–5000 | Optional. Default 1000. |

```http
GET /v1/titles/ttl_5k2m9x1qzc7v/rankings?country=US&provider=netflix
```

Each item in `data`:

| Field | Type | Description |
| --- | --- | --- |
| `date` | date | Chart day. |
| `provider` | string | Provider id. |
| `country` | string | ISO 3166-1 alpha-2. |
| `type` | movie \| tv | Which chart. |
| `rank` | integer | Position on that chart, 1–10. |
| `previous_rank` | integer \| null | Rank the day before. |
| `rank_change` | integer \| null | previous_rank − rank. Positive means it moved up. |
| `days_in_top_10` | integer | Total days on this chart. |
| `peak_rank` | integer | Best rank so far on this chart. |

Fields of `meta`:

| Field | Type | Description |
| --- | --- | --- |
| `title_id` | string | Title id. |
| `from` | date | First day returned, after clamping to your plan's history window. |
| `to` | date | Last day returned. |
| `history_days` | integer | Days of history your plan includes. |


```json
{
  "data": [
    {
      "date": "2026-09-10",
      "provider": "netflix",
      "country": "US",
      "type": "movie",
      "rank": 3,
      "previous_rank": 1,
      "rank_change": -2,
      "days_in_top_10": 13,
      "peak_rank": 1
    }
  ],
  "meta": {
    "title_id": "ttl_5k2m9x1qzc7v",
    "from": "2026-08-11",
    "to": "2026-09-10",
    "history_days": 30
  }
}
```

## Trending

<a id="trending"></a>

`GET /v1/trending`

Movement inside one provider's charts in one country: `biggest_risers`, `new_entries`, and `longest_streaks`. It compares a chart with itself, so a #1 on Apple TV and a #5 on Netflix are not comparable. For one list that ranks titles across every service, use GET /v1/trending/titles.

| Name | Type | Description |
| --- | --- | --- |
| `provider` | string | Required. |
| `country` | string | Required. ISO 3166-1 alpha-2. |
| `limit` | 1–20 | Optional. Items per bucket, default 5. |

```http
GET /v1/trending?provider=netflix&country=US
```

Fields of `data`:

| Field | Type | Description |
| --- | --- | --- |
| `provider` | string | Provider id. |
| `country` | string | ISO 3166-1 alpha-2. |
| `date` | date | Chart day. |
| `biggest_risers` | array | Ranking items from both the movie and TV charts, each with `type`. |
| `biggest_risers[].rank` | integer | Position on the chart, 1–10. |
| `biggest_risers[].previous_rank` | integer \| null | Rank the day before, or null if the title was not on yesterday's chart. |
| `biggest_risers[].rank_change` | integer \| null | previous_rank − rank. Positive means it moved up. |
| `biggest_risers[].is_new` | boolean | True on the first day a title appears on this chart. |
| `biggest_risers[].days_in_top_10` | integer | Total days on this chart, including today. |
| `biggest_risers[].peak_rank` | integer | Best rank so far on this chart. |
| `biggest_risers[].first_charted_at` | date | First day on this chart. |
| `biggest_risers[].title` | object | Canonical title: id, slug, name, type, year, tmdb_id, imdb_id, poster_url. |
| `biggest_risers[].type` | movie \| tv | Which chart. |
| `new_entries` | array | Ranking items from both the movie and TV charts, each with `type`. |
| `new_entries[].rank` | integer | Position on the chart, 1–10. |
| `new_entries[].previous_rank` | integer \| null | Rank the day before, or null if the title was not on yesterday's chart. |
| `new_entries[].rank_change` | integer \| null | previous_rank − rank. Positive means it moved up. |
| `new_entries[].is_new` | boolean | True on the first day a title appears on this chart. |
| `new_entries[].days_in_top_10` | integer | Total days on this chart, including today. |
| `new_entries[].peak_rank` | integer | Best rank so far on this chart. |
| `new_entries[].first_charted_at` | date | First day on this chart. |
| `new_entries[].title` | object | Canonical title: id, slug, name, type, year, tmdb_id, imdb_id, poster_url. |
| `new_entries[].type` | movie \| tv | Which chart. |
| `longest_streaks` | array | Ranking items from both the movie and TV charts, each with `type`. |
| `longest_streaks[].rank` | integer | Position on the chart, 1–10. |
| `longest_streaks[].previous_rank` | integer \| null | Rank the day before, or null if the title was not on yesterday's chart. |
| `longest_streaks[].rank_change` | integer \| null | previous_rank − rank. Positive means it moved up. |
| `longest_streaks[].is_new` | boolean | True on the first day a title appears on this chart. |
| `longest_streaks[].days_in_top_10` | integer | Total days on this chart, including today. |
| `longest_streaks[].peak_rank` | integer | Best rank so far on this chart. |
| `longest_streaks[].first_charted_at` | date | First day on this chart. |
| `longest_streaks[].title` | object | Canonical title: id, slug, name, type, year, tmdb_id, imdb_id, poster_url. |
| `longest_streaks[].type` | movie \| tv | Which chart. |


### Trending titles

<a id="trending-titles"></a>

`GET /v1/trending/titles`

Titles ranked across every service by a single trend score, 0–100. Three categories feed the score: `chart_momentum` (position, breadth, and movement across Top 10 charts inside the scope), `demand` (consumption and discovery intent: first-party activity is worldwide, discovery rankings follow the country scope and are summed on the global list), and `attention` (broad web attention: one worldwide feed, and reading per language that follows the country). Omit `country` for the global list; a country scope weights that country's Top 10 charts, discovery rankings, and reading in its language, while first-party demand stays worldwide. The score is daily. First-party demand counts cover the last complete day, attention is discounted when it is flat, and titles seen only by attention rank after titles with chart or demand presence.

| Name | Type | Description |
| --- | --- | --- |
| `country` | string | Optional. ISO 3166-1 alpha-2. Omit it for the global list. A country scope weights that country's Top 10 charts, that country's discovery rankings, and reading in its language; first-party demand stays worldwide. |
| `type` | movie \| tv | Optional. Restrict the list to movies or to shows. |
| `status` | new \| rising \| steady \| peaking \| fading | Optional. Keep only titles with this status. |
| `limit` | 1–100 | Optional. Default 10. Capped by your plan: 10 on Free, without a key, or with a test key; 100 on paid plans. |
| `date` | YYYY-MM-DD | Optional. A past list. Resolves to the newest list on or before the date. Requires history on your plan. |

```http
GET /v1/trending/titles
GET /v1/trending/titles?country=US&type=tv
GET /v1/trending/titles?status=rising&limit=25
GET /v1/trending/titles?country=GB&date=2026-09-10
```

Fields of `data`:

| Field | Type | Description |
| --- | --- | --- |
| `scope` | string | Country code, or `global`. |
| `date` | date | The list day. |
| `items` | array | Titles by trend score, best first. |
| `items[].rank` | integer | Position on the trending list for this scope and day. |
| `items[].previous_rank` | integer \| null | Trending rank the day before, or null if the title was not on yesterday's list. |
| `items[].rank_change` | integer \| null | previous_rank − rank. Positive means it moved up. |
| `items[].score` | number | Trend score, 0–100. Relative to every other title scored in the same scope on the same day. |
| `items[].previous_score` | number \| null | Yesterday's score in this scope, or null. |
| `items[].score_change` | number \| null | score − previous_score, one decimal place. |
| `items[].status` | new \| rising \| steady \| peaking \| fading | `new`: on this list today and not in the 7 days before. `rising`: the signals are growing against their own 7-day average and the title has already reached a meaningful level (a single new chart entry does not qualify). `steady`: little day-over-day change. `peaking`: near the top of the scale and no longer growing. `fading`: the signals are shrinking. |
| `items[].chart_count` | integer | How many Top 10 charts the title sits on today inside the scope. Counts every chart, not only the ones your plan can read. |
| `items[].on_charts` | array | Today's chart positions, filtered to the countries and providers your plan includes, so it can be shorter than chart_count. |
| `items[].on_charts[].provider` | string | Provider id. |
| `items[].on_charts[].country` | string | ISO 3166-1 alpha-2. |
| `items[].on_charts[].type` | movie \| tv | Which chart. |
| `items[].on_charts[].rank` | integer | Position on that chart, 1–10. |
| `items[].signals` | object | Category levels, each 0–100, or null when that category had no data that day. |
| `items[].signals.chart_momentum` | number \| null | Position, breadth, and movement across Top 10 charts inside the scope. |
| `items[].signals.demand` | number \| null | Consumption and discovery intent. First-party activity is worldwide and counted over the last complete day; discovery rankings follow the country scope and are summed on the global list. |
| `items[].signals.attention` | number \| null | Broad web attention, from one worldwide feed and from reading per language that follows the country (summed across languages on the global list). Discounted when flat: a title that draws the same attention every day keeps half its level, a spike keeps all of it. Attention alone never carries a title above titles with chart or demand presence unless it is spiking. |
| `items[].title` | object | Canonical title: id, slug, name, type, year, tmdb_id, imdb_id, poster_url. |

Fields of `meta`:

| Field | Type | Description |
| --- | --- | --- |
| `count` | integer | Items returned. |
| `limit_max` | integer | Largest `limit` your plan allows. |
| `history_days` | integer | Days of history your plan includes. |


```json
{
  "data": {
    "scope": "global",
    "date": "2026-09-10",
    "items": [
      {
        "rank": 1,
        "previous_rank": 4,
        "rank_change": 3,
        "score": 92.6,
        "previous_score": 81.4,
        "score_change": 11.2,
        "status": "rising",
        "chart_count": 9,
        "on_charts": [
          { "provider": "netflix", "country": "US", "type": "movie", "rank": 1 },
          { "provider": "netflix", "country": "GB", "type": "movie", "rank": 2 }
        ],
        "signals": { "chart_momentum": 96.1, "demand": 88.3, "attention": 90.7 },
        "title": {
          "id": "ttl_5k2m9x1qzc7v",
          "slug": "the-whisper-man-2026",
          "name": "The Whisper Man",
          "type": "movie",
          "year": 2026,
          "tmdb_id": 860508,
          "imdb_id": "tt11561116",
          "poster_url": "https://image.tmdb.org/t/p/w500/….jpg"
        }
      }
    ]
  },
  "meta": { "count": 1, "limit_max": 10, "history_days": 0 }
}
```

The score is daily. Charts and attention use the newest data on or before the list date. First-party demand counts cover the last complete day, so they sit one day behind. Attention is discounted when it is flat: a title that draws the same attention every day keeps half its level, a spike keeps all of it. Titles seen only by attention rank after every title with chart or demand presence unless attention is spiking. `rising` needs growth and a meaningful level, so one new chart entry on its own does not qualify.

Scope: omit `country` for the global list. A country scope weights that country's Top 10 charts and that country's discovery rankings. First-party demand and `attention` stay worldwide. Scores are only comparable inside one scope and one day.

<a id="title-trend"></a>

`GET /v1/titles/{id}/trend`

The title's row on today's list plus its score history, newest first, clamped to your plan's history window. `current` is null when the title is not trending in that scope today.

| Name | Type | Description |
| --- | --- | --- |
| `id` | string | Title id (`ttl_…`), slug, IMDb id, or numeric TMDB id. |
| `country` | string | Optional. ISO 3166-1 alpha-2. Omit it for the global scope. |
| `from` | YYYY-MM-DD | Optional start date. Clamped to your plan's history window. |
| `to` | YYYY-MM-DD | Optional end date. |
| `limit` | 1–365 | Optional. Days of history, default 90. |

```http
GET /v1/titles/ttl_5k2m9x1qzc7v/trend
GET /v1/titles/the-whisper-man-2026/trend?country=US&limit=30
```

Fields of `data`:

| Field | Type | Description |
| --- | --- | --- |
| `title_id` | string | Title id. |
| `scope` | string | Country code, or `global`. |
| `current` | object \| null | Today's row on the trending list, or null when the title is not trending in that scope today. |
| `current.scope` | string | Country code, or `global`. |
| `current.date` | date | List day. |
| `current.rank` | integer | Position on the trending list for this scope and day. |
| `current.previous_rank` | integer \| null | Trending rank the day before, or null if the title was not on yesterday's list. |
| `current.rank_change` | integer \| null | previous_rank − rank. Positive means it moved up. |
| `current.score` | number | Trend score, 0–100. Relative to every other title scored in the same scope on the same day. |
| `current.previous_score` | number \| null | Yesterday's score in this scope, or null. |
| `current.score_change` | number \| null | score − previous_score, one decimal place. |
| `current.status` | new \| rising \| steady \| peaking \| fading | `new`: on this list today and not in the 7 days before. `rising`: the signals are growing against their own 7-day average and the title has already reached a meaningful level (a single new chart entry does not qualify). `steady`: little day-over-day change. `peaking`: near the top of the scale and no longer growing. `fading`: the signals are shrinking. |
| `current.chart_count` | integer | How many Top 10 charts the title sits on today inside the scope. Counts every chart, not only the ones your plan can read. |
| `current.signals` | object | Category levels, each 0–100, or null when that category had no data that day. |
| `current.signals.chart_momentum` | number \| null | Position, breadth, and movement across Top 10 charts inside the scope. |
| `current.signals.demand` | number \| null | Consumption and discovery intent. First-party activity is worldwide and counted over the last complete day; discovery rankings follow the country scope and are summed on the global list. |
| `current.signals.attention` | number \| null | Broad web attention, from one worldwide feed and from reading per language that follows the country (summed across languages on the global list). Discounted when flat: a title that draws the same attention every day keeps half its level, a spike keeps all of it. Attention alone never carries a title above titles with chart or demand presence unless it is spiking. |
| `history` | array | One row per day, newest first. |
| `history[].date` | date | List day. |
| `history[].rank` | integer | Trending rank that day. |
| `history[].score` | number | Trend score, 0–100. |
| `history[].status` | new \| rising \| steady \| peaking \| fading | `new`: on this list today and not in the 7 days before. `rising`: the signals are growing against their own 7-day average and the title has already reached a meaningful level (a single new chart entry does not qualify). `steady`: little day-over-day change. `peaking`: near the top of the scale and no longer growing. `fading`: the signals are shrinking. |
| `history[].chart_count` | integer | Top 10 charts inside the scope that day. |
| `history[].signals` | object | Category levels, each 0–100, or null when that category had no data that day. |
| `history[].signals.chart_momentum` | number \| null | Position, breadth, and movement across Top 10 charts inside the scope. |
| `history[].signals.demand` | number \| null | Consumption and discovery intent. First-party activity is worldwide and counted over the last complete day; discovery rankings follow the country scope and are summed on the global list. |
| `history[].signals.attention` | number \| null | Broad web attention, from one worldwide feed and from reading per language that follows the country (summed across languages on the global list). Discounted when flat: a title that draws the same attention every day keeps half its level, a spike keeps all of it. Attention alone never carries a title above titles with chart or demand presence unless it is spiking. |

Fields of `meta`:

| Field | Type | Description |
| --- | --- | --- |
| `from` | date | First day returned, after clamping to your plan's history window. |
| `to` | date | Last day returned. |
| `history_days` | integer | Days of history your plan includes. |


```json
{
  "data": {
    "title_id": "ttl_5k2m9x1qzc7v",
    "scope": "US",
    "current": {
      "scope": "US",
      "date": "2026-09-10",
      "rank": 2,
      "previous_rank": 5,
      "rank_change": 3,
      "score": 90.1,
      "previous_score": 78.9,
      "score_change": 11.2,
      "status": "rising",
      "chart_count": 3,
      "signals": { "chart_momentum": 94, "demand": 86.2, "attention": 88.5 }
    },
    "history": [
      {
        "date": "2026-09-10",
        "rank": 2,
        "score": 90.1,
        "status": "rising",
        "chart_count": 3,
        "signals": { "chart_momentum": 94, "demand": 86.2, "attention": 88.5 }
      },
      {
        "date": "2026-09-09",
        "rank": 5,
        "score": 78.9,
        "status": "new",
        "chart_count": 2,
        "signals": { "chart_momentum": 81.3, "demand": 74, "attention": null }
      }
    ]
  },
  "meta": { "from": "2026-08-11", "to": "2026-09-10", "history_days": 30 }
}
```

### Title signals

<a id="title-signals"></a>

`GET /v1/titles/{id}/signals`

What moved the score, one day per row, newest first. Each category carries its 0–100 level, how many independent feeds saw the title that day, and its growth against its own 7-day baseline (clipped log growth, −1 to 1). `chart_momentum` carries `charts`, the Top 10 charts the title sat on inside the scope that day. `threshold` is the demand level a title must reach to be a pick, so you can see the day it crossed. Feeds are counted, never named. `from` is clamped to your plan's history window.

| Name | Type | Description |
| --- | --- | --- |
| `id` | string | Title id (`ttl_…`), slug, IMDb id, or numeric TMDB id. |
| `country` | string | Optional. ISO 3166-1 alpha-2. Omit it for the global scope. |
| `from` | YYYY-MM-DD | Optional start date. Clamped to your plan's history window. |
| `to` | YYYY-MM-DD | Optional end date. |
| `limit` | 1–365 | Optional. Days of history, default 90. |

```http
GET /v1/titles/ttl_5k2m9x1qzc7v/signals
GET /v1/titles/the-whisper-man-2026/signals?country=US&limit=30
```

Fields of `data`:

| Field | Type | Description |
| --- | --- | --- |
| `title_id` | string | Title id. |
| `scope` | string | Country code, or `global`. |
| `threshold` | number | Demand level a title must reach to be a pick. |
| `history` | array | One row per day, newest first. |
| `history[].date` | date | Day. |
| `history[].chart_momentum` | object | Chart level and breadth. |
| `history[].chart_momentum.value` | number \| null | Category level, 0–100. |
| `history[].chart_momentum.charts` | integer | Top 10 charts the title sat on inside the scope that day. |
| `history[].demand` | object | Consumption and discovery intent. |
| `history[].demand.value` | number \| null | Category level, 0–100, or null when it had no data that day. |
| `history[].demand.feeds` | integer \| null | Independent feeds that saw the title that day. Feeds are counted, never named. |
| `history[].demand.growth` | number \| null | Clipped log growth against the category's own 7-day baseline, −1 to 1. |
| `history[].attention` | object | Broad web attention. |
| `history[].attention.value` | number \| null | Category level, 0–100, or null when it had no data that day. |
| `history[].attention.feeds` | integer \| null | Independent feeds that saw the title that day. Feeds are counted, never named. |
| `history[].attention.growth` | number \| null | Clipped log growth against the category's own 7-day baseline, −1 to 1. |

Fields of `meta`:

| Field | Type | Description |
| --- | --- | --- |
| `from` | date | First day returned, after clamping to your plan's history window. |
| `to` | date | Last day returned. |
| `history_days` | integer | Days of history your plan includes. |


```json
{
  "data": {
    "title_id": "ttl_5k2m9x1qzc7v",
    "scope": "US",
    "threshold": 40,
    "history": [
      {
        "date": "2026-09-10",
        "chart_momentum": { "value": 94, "charts": 3 },
        "demand": { "value": 86.2, "feeds": 2, "growth": 0.31 },
        "attention": { "value": 88.5, "feeds": 1, "growth": 0.12 }
      },
      {
        "date": "2026-09-09",
        "chart_momentum": { "value": null, "charts": 0 },
        "demand": { "value": 38.4, "feeds": 1, "growth": 0.44 },
        "attention": { "value": null, "feeds": null, "growth": null }
      }
    ]
  },
  "meta": { "from": "2026-08-11", "to": "2026-09-10", "history_days": 30 }
}
```

Each category carries its level, the number of independent feeds that saw the title that day, and its growth against its own 7-day baseline. Feeds are counted, never named. `threshold` is the demand level a title must reach to be a pick, so you can read off the day it crossed.

## Countries

<a id="countries"></a>

`GET /v1/countries`

Every country with chart data and the providers available in each, plus whether your plan includes it.

```http
GET /v1/countries
```

Each item in `data`:

| Field | Type | Description |
| --- | --- | --- |
| `code` | string | ISO 3166-1 alpha-2. |
| `name` | string | Country name. |
| `providers` | array | Provider ids with chart data in this country. |
| `included_in_plan` | boolean | Whether your plan can read this country. |


```json
{
  "data": [
    {
      "code": "US",
      "name": "United States",
      "providers": [
        "netflix",
        "prime",
        "disney",
        "max",
        "hulu",
        "apple",
        "paramount",
        "peacock",
        "starz"
      ],
      "included_in_plan": true
    }
  ]
}
```

## Providers

<a id="providers"></a>

`GET /v1/providers`

Every provider we cover, the countries that have data, and whether your plan includes it.

```http
GET /v1/providers
```

Each item in `data`:

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | Provider id, used in `provider=`. |
| `name` | string | Display name. |
| `countries` | array | Countries with chart data for this provider. |
| `included_in_plan` | boolean | Whether your plan can read this provider. |


```json
{
  "data": [
    {
      "id": "netflix",
      "name": "Netflix",
      "countries": ["AU", "CA", "DE", "FR", "GB", "JP", "US"],
      "included_in_plan": true
    },
    {
      "id": "prime",
      "name": "Prime Video",
      "countries": ["…"],
      "included_in_plan": true
    }
  ]
}
```

## Channels

<a id="channels"></a>

`GET /v1/channels`

Every channel with a Prime Video Channels Top 10, the countries that have one, and whether your plan includes them.

```http
GET /v1/channels
```

Each item in `data`:

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | Channel id, used in `channel=`. |
| `name` | string | Display name. |
| `countries` | array | Countries with a chart for this channel. |
| `included_in_plan` | boolean | Whether your plan can read channel charts. |


```json
{
  "data": [
    {
      "id": "crunchyroll",
      "name": "Crunchyroll",
      "countries": ["DE", "ES", "FR", "GB", "IT", "US"],
      "included_in_plan": true
    },
    {
      "id": "discovery",
      "name": "Discovery+",
      "countries": ["IT", "US"],
      "included_in_plan": true
    }
  ]
}
```

## Alerts

<a id="alerts"></a>

Alerts tell you when a title or a market moves, so you do not poll. Set them up in the dashboard at https://streamrank.io/dashboard/alerts. Watch one title in one scope, or watch a market and receive every pick it produces.

Channels: email to your account address. Signed webhooks and Slack digests are on Pro too.

| Event | Meaning | Payload fields on top of the common ones |
| --- | --- | --- |
| `title.threshold_crossed` | A watched title's demand reached 40 with zero Top 10s in the scope. Yesterday it was below. | `threshold`, `demand`, `previous_demand`, `signals`, `status`. |
| `title.charted` | A watched title entered a Top 10 inside the scope. | `charts`: one entry per chart it entered, each `provider`, `country`, `type`, `rank`. |
| `pick.created` | A title became one of the day's ten picks. A market watch gets all ten, dated on the list day. | `position`, `score`, `score_change`, `status`, `signals`, `why`, `strength`, `premiere`, `providers`. |
| `pick.charted` | A pick landed on a Top 10 inside 30 days. Dated on the first chart date. | `pick_date`, `position`, `first_chart_date`, `first_chart_provider`, `first_chart_country`, `first_chart_rank`, `days_to_chart`. |

Every payload also carries `scope`, `date` (the chart or list day the event is about), and `title` (the canonical title object).

### Webhooks

One POST per event, JSON body:

```json
{
  "id": "evt_1842",
  "type": "pick.created",
  "created_at": "2026-09-15T11:05:12.431Z",
  "data": {
    "type": "pick.created",
    "scope": "US",
    "date": "2026-09-15",
    "position": 1,
    "score": 45.4,
    "score_change": 5.2,
    "status": "rising",
    "signals": { "chart_momentum": 0, "demand": 83.1, "attention": 45.2 },
    "why": [
      "Lands Sep 19 on Netflix.",
      "Demand 83 of 100.",
      "On no Top 10 in United States."
    ],
    "strength": "strong",
    "premiere": "2026-09-19",
    "providers": ["netflix"],
    "title": {
      "id": "ttl_9q2h7v3zkm1d",
      "slug": "obsession-2026",
      "name": "Obsession",
      "type": "movie",
      "year": 2026,
      "tmdb_id": 1364532,
      "imdb_id": "tt31520893",
      "poster_url": "https://image.tmdb.org/t/p/w500/obsession.jpg"
    }
  }
}
```

`data` is the payload for the event type. Headers:

```http
X-StreamRank-Signature: t=<unix seconds>,v1=<hex>
X-StreamRank-Event: pick.created
X-StreamRank-Delivery: dlv_1842
```

`v1` is HMAC-SHA256 over `${t}.${rawBody}` with your endpoint secret. Compare it in constant time against the exact bytes you received, not a re-serialized body.

```javascript
import crypto from "node:crypto";

// rawBody must be the exact bytes you received, before JSON.parse.
function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(parts.v1 ?? "", "hex");
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return false;
  // Reject anything older than five minutes.
  return Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
}
```

Respond 2xx within 10 seconds. A failed delivery is retried on the next run, up to 3 attempts. After 10 consecutive failures the endpoint is paused; re-enable it in the dashboard.

### Slack

One digest post per day per endpoint, with every event since the last post.

## Errors

<a id="errors"></a>

Errors are JSON with a stable `code` and a human-readable `message`.

```json
{
  "error": {
    "code": "plan_required",
    "message": "Country \"JP\" is not included in the sample scope without an API key.",
    "details": {
      "param": "country",
      "requested": "JP",
      "allowed_countries": ["US"],
      "hint": "Use country=US, or send a key for a plan that includes JP.",
      "upgrade_url": "https://streamrank.io/pricing",
      "docs_url": "https://streamrank.io/docs.md#authentication"
    }
  }
}
```

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | Missing or malformed parameter. details.hint says how to fix it. |
| 401 | `unauthorized` | Malformed or revoked API key. Omit the header for the sample scope. |
| 403 | `plan_required` | The country, provider, or date is outside your plan. details lists allowed values and upgrade_url. |
| 404 | `not_found` | No chart or title for that request. For titles, search GET /v1/titles?q=. |
| 429 | `rate_limited` | Per-minute limit hit. Honor Retry-After. Check X-RateLimit-Reset. |
| 429 | `quota_exceeded` | Monthly request quota hit. Honor Retry-After or upgrade. |
| 500 | `internal_error` | Our fault. Retry with backoff. |

Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`. 429 responses also send `Retry-After`. Every error includes `details.docs_url` and, when useful, `details.hint` with the next legal call.

## MCP

<a id="mcp"></a>

Assistants can use StreamRank as a remote MCP server (Streamable HTTP) at https://streamrank.io/mcp. Each tool calls the matching endpoint above.

- `list_coverage`: Streaming services StreamRank tracks and the countries each one has a daily Top 10 in.
- `get_chart`: The daily Top 10 for one streaming service in one country, with rank change, days in the Top 10, and peak rank for each title.
- `get_chart_movers`: Biggest risers, new entries, and longest streaks on one service's Top 10 in one country.
- `get_trending_titles`: Titles ranked by demand across services (charts, search, social, and catalog signals), with a status of new, rising, steady, peaking, or fading.
- `get_breakouts`: StreamRank's daily picks: titles with demand behind them that are not on a streaming Top 10 yet, plus the public hit rate of past picks.
- `search_titles`: Find a movie or show by name, IMDb id, or TMDB id. Returns ids to pass to get_title.
- `get_title`: One movie or show: where it is on the Top 10s right now and its chart stats.
- `get_title_rankings`: Day-by-day Top 10 positions for one title, optionally for one service and country. History depth depends on the plan.

Clients that support MCP sign-in (Claude, ChatGPT, Claude Code, Cursor, VS Code) ask you to sign in with your StreamRank account the first time. Tools then use your plan: the sample scope (US Netflix) on Free, everything on Pro, which starts with a free trial. Clients without sign-in can send Authorization: Bearer sk_live_… or connect to https://streamrank.io/mcp?key=sk_live_…. Treat that URL like the key. Tool calls count toward the same monthly quota as direct API calls.

```bash
claude mcp add --transport http streamrank https://streamrank.io/mcp
```

```json
{
  "mcpServers": {
    "streamrank": {
      "url": "https://streamrank.io/mcp"
    }
  }
}
```

## SDKs

The API is plain HTTP. Official SDKs are not published yet.

```javascript
const res = await fetch("https://streamrank.io/v1/trending/breakouts?country=US");
const { data } = await res.json();
console.log(data.items[0].title.name, data.items[0].why);
```

```python
import requests

r = requests.get("https://streamrank.io/v1/trending/breakouts", params={"country": "US"})
for item in r.json()["data"]["items"]:
    print(item["position"], item["title"]["name"], item["why"])
```
