Milarki has an API now
The release post gave this about eighty words, filed under "one more, for the tinkerers." That undersells it.
Everything else in Tournament 2.0 made something better — formats you couldn't run before, a page that tells you what to do next, chat where the event is. The API is the only part that lets you do something you couldn't do at all: take Milarki's data somewhere else and build with it.
It's live, it's read-only, and it's ten endpoints.
What you can read
Tournaments — list them, fetch one, get a stage's standings, list the participants, list the battles played.
Clubs — fetch a club, list its tournaments, list its battles.
Battles — fetch a single battle.
Players — list a player's battle history.
That's the whole surface. Every endpoint is a `GET`. Nothing you do here can change anything in Milarki, which means the worst thing a bug in your code can do is fetch the same page twice.
Getting a key takes about a minute
Go to Profile → API and generate one. You can hold up to five of them.
Send it as a bearer token.
What comes back
Everything comes back as JSON, wrapped in data — a single object for one thing, an array for a list. Each entity carries a url pointing at its own page on Milarki, so a result can always be linked back to where it lives:
json
{
"data": {
"id": "9f2c...",
"name": "Meridia Open 2026",
"url": "https://milarki.com/tournaments/meridia-open-2026"
}
}
Lists are paginated with cursors. Each page hands you a next — the full URL of the following page, or null when you've reached the end:
json
{
"data": [ ... ],
"next": "https://milarki.com/api/v1/tournaments?cursor=eyJpZCI6Im5leHQifQ&limit=20"
}
Follow next rather than building cursors yourself — they're opaque and they will change.
And when something goes wrong, you get a proper HTTP status code with a JSON body explaining what happened, rather than a 200 quietly hiding a failure.
Three decisions worth knowing about
Most of what makes an API pleasant or miserable is decided before the first endpoint is written. Three we made deliberately:
Names, not internal ids. Anything you can go and fetch is referred to by its address — a tournament and club by slug, a battle by id, a player by `playerId`. Anything you can't fetch is simply spelled out. So a battle tells you its scenario in words, but points at its tournament by slug, because the tournament is somewhere you can actually go next. You shouldn't need a lookup table to read a response.
One shape per thing. A tournament looks identical whether you fetched it on its own, found it in a list, or reached it through a club. Same for battles and army lists. Write your parser once.
Links included. Tournaments, clubs and battles all carry a `url` to their page on milarki.com. Whatever you build, you can always send someone to the real thing.
Stages are the interesting part
Tournament 2.0 made tournaments multi-stage — Swiss into a knockout, group stages, placement brackets. The API exposes that honestly rather than flattening it into one table.
A tournament carries a `stages` array in the order they run. The leaderboard endpoint takes `?stage=` and gives you that stage's standings. Leave it off and you get the final standings once the tournament has finished, or whichever stage is furthest along if it hasn't.
Two things to get right:
Read the stage ids from the tournament, don't guess them. A tournament only has the stages it was actually set up for.
Don't build the display name from the id. The `name` that comes back is word for word the label the organiser sees in Milarki, which is what makes a screen built on the API agree with the tournament page next to it. The same `swiss` id comes back as "Swiss Stage" in one event and "Group Stage" in another, depending on how it was set up.
And one thing that looks like a bug and isn't: on a group stage board, `rank` restarts at 1 in every group. Whoever won Group B didn't tie with whoever won Group A — they never played the same set of people.
Leaderboards come back whole, by the way. No pagination. A board is one row per player and a bracket is a handful of matches, so you always get the lot in one call.
Limits, honestly
20 requests per minute. 300 per day. Per account, not per key — your five keys share one budget. Both are fixed clock buckets in UTC rather than rolling windows.
Every response carries `X-RateLimit-Remaining-Minute`, `X-RateLimit-Remaining-Day` and `X-RateLimit-Reset`, including the 429 that rejects you, so you can always read your remaining budget off the last response. A 429 also carries `Retry-After` in seconds.
401s and 429s don't spend anything. A 400 or a 404 does — the request is counted as soon as it passes auth, before the endpoint runs.
What this means in practice: cache, and poll slowly. A stream overlay hitting the leaderboard every five seconds will burn the day's budget before the second round finishes. Poll it every couple of minutes across an eight-hour event and you've spent around 240 calls — comfortably inside the day.
If your integration genuinely needs more, get in touch. Limits are raisable per account, and we'd rather hear from you than watch you get banned by the abuse detection.
Things worth building
Some of these already exist as spreadsheets someone maintains by hand on a Sunday night:
- Live standings on an event's own website. One call, no pagination, done.
- A stream overlay that follows the current stage instead of being retyped at nine on Saturday morning.
- A club's whole season in a spreadsheet — its tournaments, its battles, its players.
- Meta analysis. Faction results across a season, pulled rather than typed. We did this by hand for the GHB review; you shouldn't have to.
- A player's record on their own site, kept current without them touching it.
Tell us what you make
The docs live at [milarki.com/docs/api](https://www.milarki.com/docs/api) — getting started, authentication, pagination, errors, rate limits, and a reference page for every object the API returns.
If you build something, bring it to the Discord. We'll write about it. And "v1" is doing real work in that URL — if there's data you need that isn't there, that's exactly what we want to hear before we design v2.
The door's open.

