Endpoints
Six 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.
index.json for pastSeasons to know which exist.
Drawn to scale. Seeing who sits where costs a seventy-sixth 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",
"pastSeasons": [],
"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,
"rankHistory": [{ "d": "2026-08-16", "r": 137 },
{ "d": "2026-08-17", "r": 139 }],
"usageHistory": [{ "d": "2026-08-17",
"m": { "Protect": 77.2 },
"i": { "Life Orb": 18.7 },
"a": { "Intimidate": 54.6 } }],
"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.
"M-5". Seasons run about four weeks and the ranking restarts with each one, so a capture from one season is not comparable with a capture from another.
/api/v1/seasons/{season}/ exactly as it stood on its final capture. Empty until the first season ends.
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.
d the UTC day, r the rank. One entry per day, ending on the day of this capture, and the last r is always rank. A day it was not ranked is missing rather than null. Present on /api/v1/pokemon/{id}.json only — it is left out of all.json and ranking.json to keep them small — and absent from files published before it existed.
d the UTC day, then m moves, i items and a abilities, each an object of name to percentage. Only the head of each list is kept — eight moves, six items, three abilities — and a name missing from a day was not in that day's head. One entry per day, the last being this capture's reading, and a new season starts a new series because the percentages are of a different ladder. Present on /api/v1/pokemon/{id}.json only, and absent from files published before it existed.
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. beatsGames and losesToGames are the number of games behind each entry, aligned with the list beside them — beatsGames[2] belongs to beats[2]. They are games won or lost, not games played: a pair's total is the figure from both directions. Absent on a reading captured before the game began publishing them, and null per row where it published none.
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.
There is a ladder-top.json beside each of these and it is not
listed above on purpose. It is the ten rows the front page shows instead of
pulling the whole file, which makes it a decision about a layout rather than
a dataset — naming it here would freeze "ten" into a contract that only
/api/v2 could change. It is reachable, it is simply not promised.
Slice entries and pick your own number.
Singles is at /api/v1/singles/ladder.json, in the
same shape, with "format": "singles" in its head. It is a
genuinely different ladder rather than the same players sorted twice: across
one capture the two share 7 trainers out of a thousand, have
different leaders, and singles rates higher at the top — 2383 against 2344.
It exists because it costs nothing. A cold start opens the client on Singles and the capture walk switches it to Doubles, so both trainer files arrive in one capture and only one of them used to be read. Nothing on this site shows it, and there is no singles usage data for the same reason there is no cost here: that one would need its own walk through the client every hour.
{
"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.
Using it, and crediting it
Free to use, in anything — a tool, a spreadsheet, a video, a paid product. No key, no sign-up, no quota. Two things asked in return.
Say where it came from. If your project shows these numbers, name MetaRoll as the source and link to metaroll.app somewhere a reader can find it — a footer, an about page, a video description. That is the whole licence. It is asked rather than enforced, and it is what keeps a capture rig running on someone's machine every hour worth doing.
Cache what you fetch. This is served off a small
machine on a home connection. Every response carries
Cache-Control: public, max-age=300; honouring it costs you
nothing, because the data cannot change faster than the capture that
produces it. If you need the whole dataset, take
all.json once rather than 235 files in a loop.
What the credit cannot do is imply an endorsement. This is an independent fan project reading a game it has no relationship with, and anything built on it inherits that: name MetaRoll as a source, not as a partner, and do not present the numbers as official.