MetaRoll

Pokémon Champions · Season M-5 · Read-only JSON

Champions Usage API

The game's own ranked ladder — all 235 ranked Pokémon with their moves, held items, abilities, natures, Stat Point spreads and common partners — as static JSON. No key, no rate limit, CORS open to any origin.

Basehttps://metaroll.app/api/v1

Endpoints

Five files. Every one repeats the fields of index.json, so a single request tells you both the data and how old it is. The last of them ranks players rather than Pokémon — see Trainer ladder.

GET/api/v1/index.json 291 B What this is, when it was captured, what the movement is measured against.
GET/api/v1/ranking.json 16 kB The ladder alone: rank, id, name and previous rank for all 235.
GET/api/v1/pokemon/{id}.json ≈4 kB One Pokémon in full — every distribution the game publishes for it.
GET/api/v1/ladder.json 327 kB The trainer ladder: the top 1000 players, with rating, record and country.
GET/api/v1/all.json 937 kB Every entry in one file, for when you want the lot.
ranking.json 16 kB
one Pokémon 4 kB
all.json 937 kB

Drawn to scale. Seeing who sits where costs a fifty-eighth of the whole file; looking one Pokémon up costs a two-hundredth. That is the only reason the data is split at all.

Fetching

BASE=https://metaroll.app

curl -s $BASE/api/v1/index.json

curl -s $BASE/api/v1/ranking.json \
  | jq -r '.entries[:5][] | "\(.rank)  \(.name)"'

curl -s $BASE/api/v1/pokemon/kingambit.json | jq '.entry.moves[:3]'
// Works from a browser on any origin — Access-Control-Allow-Origin: *
const res = await fetch('https://metaroll.app/api/v1/pokemon/kingambit.json')
const { entry, capturedAt } = await res.json()

entry.rank          // 1
entry.previousRank  // 1
entry.moves[0]      // { name: "Sucker Punch", percent: 99.3 }

Naming a Pokémon

An id is the name lowercased, with every run of non-alphanumeric characters collapsed to a single hyphen. Build the path from a name you already have — there is no lookup step.

Kingambit            → kingambit
Tauros-Paldea-Aqua   → tauros-paldea-aqua
Mr. Rime             → mr-rime
Ninetales-Alola      → ninetales-alola

Alternate forms are separate entries with separate ids. The ranking really does list four Gourgeist sizes and six Rotom forms in different places.

A full response

GET /api/v1/pokemon/tauros-paldea-aqua.json — trimmed to the first item of each list.

{
  "version": 1,
  "game": "Pokémon Champions",
  "source": "in-game ranked ladder",
  "format": "doubles",
  "season": "M-5",
  "capturedAt": "2026-08-17T06:27:02.238Z",
  "comparedTo": "2026-08-16T14:01:20.495Z",
  "count": 235,
  "entry": {
    "id": "tauros-paldea-aqua",
    "name": "Tauros-Paldea-Aqua",
    "rank": 139,
    "previousRank": 137,
    "abilities": [{ "name": "Intimidate", "percent": 54.6 }],
    "items":     [{ "name": "Life Orb", "percent": 18.7 }],
    "moves":     [{ "name": "Protect", "percent": 77.2 }],
    "natures":   [{ "name": "Adamant", "percent": 60 }],
    "teammates": [{ "name": "Farigiraf", "percent": null }],
    "matchups": {
      "beats":     ["Sneasler", "Sylveon"],
      "beatsWith": [{ "name": "Earthquake", "percent": 48.2 }],
      "losesTo":   ["Basculegion", "Archaludon"],
      "beatenBy":  [{ "name": "Blizzard", "percent": 6.2 }]
    },
    "spreads": [{
      "statPoints": { "hp": 2, "atk": 32, "def": 0,
                       "spa": 0, "spd": 0, "spe": 32 },
      "percent": 29.1
    }],
    "confidence": {
      "formGuessed": false,
      "identifiedByElimination": false
    }
  }
}

Field reference

On every response

versionnumber The contract version. 1 will not change shape; a breaking change becomes /api/v2.
capturedAtstring · ISO 8601 When this reading of the ladder was taken. Use it to decide whether to re-fetch.
comparedTostring · ISO 8601 · optional The capture previousRank is measured against. Absent when there was nothing to compare.
formatstring Which ranked ladder this is. Always "doubles": the game ranks doubles and singles separately, and this data is read from the doubles ladder only.
countnumber How many Pokémon the ladder ranked. 235 for season M-5.

On an entry

ranknumber Position on the ladder, 1 being the most used.
previousRanknumber | null · optional null means it was not in the previous capture. The field is absent entirely when there is no previous capture — so you can tell a new arrival from an unknown.
abilities · items · moves · natures · teammates{ name, percent }[] Ordered most-used first. percent is a share of that Pokémon's appearances, not of the ladder.
matchups{ beats, beatsWith, losesTo, beatenBy } beats and losesTo are species names, most-frequent first. The two move lists belong to different Pokémon: beatsWith are this Pokémon's own moves, and beatenBy are the moves its opponents beat it with. Their percentages are a share of those wins or losses, not a win rate.
spreads{ statPoints, percent }[] Stat Point distributions, most-used first. See the warnings below — these are not EVs, and they carry no nature.
confidence{ formGuessed, identifiedByElimination } Both are false on current data: the game serves the form outright, so nothing is inferred. Kept for readers who already check them.

Trainer ladder

/api/v1/ladder.json ranks players, not Pokémon. Same capture, same ranked doubles ladder, a different thing counted — so it carries its own capturedAt and its own comparedTo, and moves far more than the Pokémon ranking does: 789 of the thousand changed rank between two readings a day apart.

The game shows the top 300 in its own client. It downloads a thousand, and a thousand is what is published here.

{
  "version": 1,
  "source": "in-game ranked ladder",
  "format": "doubles",
  "ranks": "trainers",
  "capturedAt": "2026-08-19T08:16:09.000Z",
  "comparedTo": "2026-08-18T06:25:09.000Z",
  "count": 1000,
  "entries": [
    {
      "rank": 1,
      "previousRank": 4,
      "id": "biAcxGh7EW2hA1lfddc8abf5bd2cbbae",
      "name": "Nontaro",
      "rating": 2275.236,
      "wins": 262,
      "losses": 154,
      "draws": 0,
      "battles": 416,
      "winRate": 63.0,
      "country": { "code": 509, "name": "Thailand", "flag": "🇹🇭", "iso": "th" },
      "language": { "code": 20, "name": "English", "flag": "🇺🇸", "iso": "us" },
      "avatar": 22
    }
  ]
}

Three things that will catch you

idstring Track a player by this and never by name. Names are not unique — 972 distinct across 1000 rows — and are the reason movement here is keyed on the id.
countryobject code is a region code, not a country code: the leading digit is a continental block, and x99 means "Other" inside it, so seven different codes all mean "Other". The USA holds four (202–205). Group by name, never by code. iso is ISO 3166-1 alpha-2, and is absent on "Other".
winRatenumber · nullable Wins over battles as a percentage, and null rather than zero for a player with no games. Both it and battles are derived — the game stores only W, L and D — and are published so every reader does not divide by zero separately.
ratingnumber Already divided: the game stores it multiplied by a thousand. This is the ladder's sort key.
avatarnumber The Pokémon icon the player picked, in the game's own numbering — which is not the national dex. Published raw; no image is served for it.

Before you build on it

Four things that will cost you an afternoon if you meet them by surprise.

statPoints are Stat Points, not EVs. Champions replaced EVs with Stat Points: 0–32 per stat, 66 across all six. Feed them to an EV formula and every number you produce is wrong, with nothing in the data to signal it.

Spreads carry no nature. The game publishes natures and spreads as two separate distributions rather than as observed pairs — not one of the 7,020 spreads in this capture carries a nature. natures is the whole of what the game says on the subject.

This differs from Smogon usage stats, where nature and spread come paired. Joining them here would invent data that looks measured.

teammates always has percent: null. The game ranks a Pokémon's common partners without quantifying them. That is the source being silent, not a reading that failed.

confidence is now always false, and that is the point. The ladder shows a species' alternate forms under one shared name and picture — all four Gourgeist sizes read No. 711 Gourgeist on screen — so while these entries were read off the screen the forms had to be inferred, and inference was sometimes wrong: two Paldean Tauros were once published under each other's names.

The data the game serves carries the form outright, so nothing is guessed any more. Both flags are kept, and kept false, rather than removed: a field that vanishes breaks whoever was reading it.

Where the data comes from

Pokémon Champions publishes no usage API. It does serve its own Battle Data to its own client, though, and that is what this is: a capture drives the game, records the ranking files it fetches, and decodes them. The numbers below are the ones the game published, not a reading of them — the ids it uses for moves, abilities and species turn out to be the canonical Pokémon indices, and the Stat Point spreads are hexadecimal.

Earlier readings were taken by photographing the Battle Data screens and running OCR over the frames, which is why confidence exists. It is kept, and reports false throughout, because a field that disappears breaks anyone reading it — but there is nothing left to be unsure about.

capturedAt is the moment the reading was taken. Captures run hourly and publish only when something has changed: the game rebuilds this data every fifteen minutes or so, but between two consecutive builds the rank order was identical and only decimals deep in the lists moved.

The ladder itself moves slowly. Over a full day, 98 Pokémon changed position and the largest move was five places.