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.
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
1 will not change shape; a breaking change becomes /api/v2.
previousRank is measured against. Absent when there was nothing to compare.
"doubles": the game ranks doubles and singles separately, and this data is read from the doubles ladder only.
On an entry
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.
percent is a share of that Pokémon's appearances, not of the ladder.
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.
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
name. Names are not unique — 972 distinct across 1000 rows — and are the reason movement here is keyed on the id.
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".
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.
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.