THE FINALS API
Getting started
Everything this site shows publicly is available as JSON. There is no API key and no
sign-up for any of it. Every endpoint is a GET, CORS is open to all origins, so you can call
it straight from a browser, a bot, a spreadsheet or a script.
One exception: the ownership endpoints, which resolve an Embark account to the Discord user who owns it, need a key we issue by hand. Everything else is open.
Base URL https://api.finalstracker.net/v1 curl "https://api.finalstracker.net/v1/leaderboard?limit=5" curl "https://api.finalstracker.net/v1/players/Balise-2431"
Identifying a player. An Embark ID looks like Balise#2431. A #
cannot survive a URL round trip (it is a fragment delimiter, and tools that decode it drop everything after it),
so in a path the # becomes a -: Balise-2431. Percent-encoded
Balise%232431 also works, but the dashed form is the one that survives being pasted into things.
Every response carries a meta block saying which board it came from.
Data refreshes on a roughly 10 minute snapshot cycle, and responses are cached for up to 60 seconds, so
generatedAt is when the body was built and snapshotAt is when the underlying board
was read. Polling faster than the snapshot interval returns the same numbers.
"meta": {
"season": "s11",
"snapshotId": 7649,
"snapshotAt": "2026-08-15T02:20:12.792Z",
"boardSize": 10000,
"generatedAt": "2026-08-15T02:31:04.118Z"
}
Versioning. The version lives in the path. Fields are only ever added within
v1; a field that ships here keeps its name and its meaning. Anything that would break an existing
integration goes in a v2 instead. Write your client to ignore fields it does not recognise.
Errors are JSON with an HTTP status: 400 a bad parameter,
404 no such player or endpoint, 429 rate limited, 503 a snapshot is
being written, retry shortly. The body is
{ "error": "not_found", "message": "…", "docs": "…" }.
Rate limits and fair use
300 requests per minute per IP. A few endpoints that run uncached text searches are
tighter: the leaderboard is 240/min, player history 120/min, and /search 60/min. Exceeding a limit
returns 429 with a Retry-After header. There is no daily cap.
This runs on one small server that also serves the website, so please be a considerate neighbour: cache what you fetch, page through the board rather than hammering it, and do not poll faster than the ~10 minute snapshot cycle, since you will only get the same numbers back. If you are building something that needs more, email us and we will work it out rather than rate limiting you into a corner.
Status and leaderboard
The current season, the live snapshot, the Ruby and top-10,000 cutoffs, how many accounts are active right now, and totals for everything tracked.
The live ranked top 10,000, paged and filterable.
| Parameter | Default | Notes |
|---|---|---|
| limit | 100 | 1 to 500. |
| offset | 0 | Page through with total. |
| q | none | Matches Embark name, Steam / PSN / Xbox handle, or club tag. |
| league | none | Exact league, for example Ruby or Diamond 2. |
| activity | none | active, offline-soon, idle. |
| sort | rank | rank, rankScore, change24h, name. |
| include | board | tail also returns estimated rows below rank 10,000. See Estimated values. |
{
"total": 10000, "limit": 2, "offset": 0, "include": "board", "sort": "rank",
"players": [
{
"rank": 1,
"embarkId": "Balise#2431", "name": "Balise", "tag": "2431",
"league": "Ruby",
"rankScore": 71422,
"change24h": { "rank": 0, "rankScore": 412 },
"changeSinceLastSnapshot": 0,
"platforms": { "steam": "Balise", "psn": "TTV-Balise", "xbox": null },
"club": "BERGR",
"activity": "active",
"offBoard": false, "estimated": false, "lastKnown": null,
"ownership": { "claimed": false, "verified": false, "discordUserId": null,
"embarkUserId": null, "linkedAt": null },
"profileUrl": "https://finalstracker.net/p/Balise-2431"
}
],
"meta": { }
}
Players
One account in full: where it stands, this season's derived record, its career across
past seasons, badges, name history and ownership. standing.state is the field to branch on:
onBoard, offBoard, banned, historic (placed in a season
that predates this tracker, so there is no time series) or untracked (a real, claimed account
that has never placed top 10,000).
If the ID was renamed away, this returns 301 with the new ID in
renamedTo and a Location header, so a client holding an old ID can follow it in
one hop.
{
"player": {
"embarkId": "PlayrTTV#0245", "name": "PlayrTTV", "tag": "0245",
"standing": {
"state": "onBoard", "rank": 764, "rankScore": 45569, "league": "Diamond 2",
"change24h": { "rank": -34, "rankScore": -136 },
"activity": null,
"platforms": { "steam": "Playr", "psn": null, "xbox": null },
"club": "BUNNX"
},
"season": {
"id": "s11", "peakRank": 88,
"record": {
"wins": 21, "losses": 8, "moves": 29, "netRankScore": 4510,
"winRate": 0.7241, "winRateSampleFloor": 20,
"streaks": { "currentWin": 0, "currentLoss": 2, "bestWin": 6, "worstLoss": 3 },
"firstSeenAt": "2026-06-22T13:13:57.069Z",
"lastActiveAt": "2026-08-14T22:50:12.327Z",
"adjustmentThresholdRs": 3500
}
},
"career": {
"seasons": [ { "season": "s9", "rank": 7654, "basis": "final" } ],
"seasonsPlayed": 6, "bestRank": 88
},
"seasonRecords": [ ], "placements": [ ],
"names": { "former": [ ], "renamedTo": null },
"badges": [ { "slug": "og", "title": "OG", "rarity": "legendary" } ],
"ownership": { "claimed": true, "verified": true,
"linkedAt": "2026-08-14T23:00:13.151Z",
"discordUserId": "restricted", "embarkUserId": "restricted" }
},
"meta": { }
}
What a "win" is. Embark's leaderboard publishes no win/loss record, so ours is
derived: one move is a snapshot interval in which the account's rank score changed, and its sign
makes it a win or a loss. Embark's nightly adjustment batch is excluded. A quiet twenty minutes containing
two matches counts as one move, so treat these as a shape, not as official match data.
winRate is withheld (null) below winRateSampleFloor moves, because
one win and no losses is not a 100% win rate.
career[].basis matters. best means the highest rank the
account ever held in a season we tracked from end to end. final means where it finished, which
is all a single archived end-of-season board can tell us. Comparing one against the other is meaningless, so
the field is there to stop you doing it by accident.
Rank score over time as change points: one entry per moment the score actually
moved, which draws an exact step line at a fraction of the size. Spans the account's whole rename chain, so
a player who renamed mid-season still returns one continuous series, with recordedAs naming
which identity each point was recorded under. adjustment: true marks a point Embark moved
rather than one the player earned.
| Parameter | Default | Notes |
|---|---|---|
| season | all | For example s11. |
| from, to | all | ISO timestamps, inclusive. |
| limit | 5000 | Up to 20000. truncated: true if it capped. |
{
"embarkId": "Balise#2431", "names": ["Balise#2431"],
"points": [
{ "at": "2026-06-22T13:13:57.069Z", "rank": 14, "rankScore": 65389,
"season": "s10", "recordedAs": "Balise#2431", "adjustment": false }
],
"count": 1, "truncated": false, "meta": { }
}
Searches the permanent account registry: every account that has ever placed in a
top 10,000 we hold, including seasons that predate this tracker. tracked: false means it exists
only as a historic placement, with no time series behind it. Minimum 2 characters.
Seasons and cutoffs
Every season held, each with a tier saying what we actually have for it:
live, tracked (watched end to end), partial (tracking started
mid-season) or final-only (one archived end-of-season board). Read it before presenting a
season's numbers as complete.
A season's final top 10,000, paged (limit, offset,
q). Note rankScore is null for open beta (ob), which
ranked on fame rather than rank score. That is not missing data.
Cutoff history plus the projected final. board=ruby (default) tracks the
rank score of the player sitting at rank 500, since Ruby is positional. board=top10k tracks the
score needed to stay on the board at all. season= defaults to the live one.
⚠ forecast.predictedFinal is a curve fit, not a measurement. Check
forecast.isActive: for a finished season the real final is the last point of
history, and no prediction should be shown.
Adjustments, renames and disappearances
Embark's nightly rank score adjustment batches, one entry each, with how many accounts moved and the split of gains to losses.
The individual accounts inside those batches. Filter with at= (a batch
timestamp) or player= (an Embark ID).
Detected display name changes. confidence is strong (a
distinctive platform handle followed the player), corroborated (a common handle, but rank score
or club agrees) or weak. If you are merging two accounts on the strength of this, require
strong.
⚠ Embark publishes no ban list, and neither do we. These are accounts that
vanished from a rank too high for "dropped out of the top 10,000" to explain it, which is an inference. The
field is called detection for that reason, and every response repeats the disclaimer. Please do
not present these as official sanctions. /v1/unbans lists flagged accounts that later
reappeared, which is the honest correction to the ones that were wrong.
Tournaments and badges
Tracked community events and majors, with final standings, rosters, prize pools and the badges awarded for them.
Badge definitions and holder counts, and for a single badge the full holder list with award dates.
Ownership key required
Players can link their Embark ID to their Discord account on finalstracker.net, proving ownership either through our browser extension (which reads the ID from Embark itself) or an in-game code challenge (which we read back off the public leaderboard). Both are verified at a source the claimant does not control.
Whether an account is verified is public. Who verified it is not. Every player
object carries an ownership block saying whether the account has been claimed and proved, which
is the same thing the checkmark on the leaderboard already says. The owner's Discord user ID is
restricted: a Discord ID resolves to a real person, and publishing it beside an in-game identity would
let anyone build a complete Discord-to-Embark directory of every player who ever verified, just by paging
through the leaderboard. Without a key those two fields read
"restricted" rather than a value.
// no key
"ownership": { "claimed": true, "verified": true,
"linkedAt": "2026-08-14T23:00:13.151Z",
"discordUserId": "restricted", "embarkUserId": "restricted" }
// with an ownership-scoped key
"ownership": { "claimed": true, "verified": true,
"linkedAt": "2026-08-14T23:00:13.151Z",
"discordUserId": "743956656429203535", "embarkUserId": null }
Getting a key. Access is granted by hand, to named parties with a reason to resolve claims: tournament organizers checking that a sign-up is really the account it says it is, and community bots assigning verified roles. Email [email protected] with who you are, what you are running and what you need it for. Keys are free, revocable, and tied to you.
Send it as a header. Never in a query string, which would put it in logs and browser history:
curl -H "Authorization: Bearer ftk_your_key_here" \ "https://api.finalstracker.net/v1/ownership/embark/PlayrTTV-0245"
Treat it as a secret: server side only, never shipped in a web page or a public repo.
401 key_required means no key was seen, 403 insufficient_scope means the key is
valid but not for this. GET /v1 echoes your key's label and scopes back, which is the quickest
way to confirm it is arriving.
verified is the field that matters. true means the claim
was proved at the source, by one of the two methods above. false means it is one of a handful of
links made before August 2026 under an older, optimistic method that took the claimant's word for it. Both
show a checkmark on the site; only this field tells them apart. If you are granting anything on the strength
of a claim, require verified: true.
Linking is optional for players, and unlinking on the account page removes them from these responses immediately. See the Privacy Policy.
Which Discord user owns this Embark ID.
{
"embarkId": "PlayrTTV#0245",
"claimed": true,
"verified": true,
"linkedAt": "2026-08-14T23:00:13.151Z",
"discordUserId": "743956656429203535",
"embarkUserId": null,
"meta": { }
}
The reverse: which Embark ID this Discord user has linked. Returns
linked: false if they have not linked one.
Every claim in one paged list, so a role-sync bot does not have to walk the whole
leaderboard to find the few it cares about. verified=true returns only proved claims.
Estimated values, and how to not misreport them
Embark publishes only a top 10,000. An account below that line is invisible to everyone,
including us. Rather than have players vanish, the leaderboard can be extended past the cutoff with
include=tail, and a player profile reports the same thing in
standing.estimatedRank.
On those rows, both headline numbers are estimates, and they are flagged
estimated: true so you can label them. rank is our ordering of accounts
that recently fell off, not a position Embark published. rankScore is the top-10,000
cutoff the account fell under, never a score it actually held. What it really last held is in
lastKnown. Presenting an estimated rank score as a player's real rating is the single easiest
mistake to make with this API, which is why it has its own section.
Two smaller ones worth knowing: a historic player has placements but no time
series, so charts should say so rather than render empty; and tail rows carry no league or platform handles,
because we do not retain them for accounts that have left the board.
Terms of use
Free to use, including commercially. In return, two asks and one rule:
- Credit finalstracker.net where the data is shown, with a link if the medium allows one. Some of this (derived records, activity, cutoff history, tournament badges) exists nowhere else and was built here.
- Do not present derived or estimated figures as official. Win/loss, activity, cutoff projections, estimated ranks and ban detections are ours, not Embark's. The relevant sections above say which is which.
- No scraping around the API. If you need bulk data, page through these endpoints. Hammering the website's HTML instead, or trying to route around the rate limit, is the one thing that will get an IP blocked.
The API is offered as-is, with no uptime guarantee. It is a community project, run on one server, and it can go down or change. The site's Terms of Service apply.
This is unofficial and not affiliated with or endorsed by Embark Studios. THE FINALS is a trademark of Embark Studios AB.
Changes and contact
Additions are announced in the Discord. Questions, bug reports, a higher rate limit, or a request for data that is on the site but not yet in the API: [email protected] with API in the subject. If you build something with it, we would genuinely like to see it.