← Leaderboard

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

GET /v1/status

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.

GET /v1/leaderboard

The live ranked top 10,000, paged and filterable.

ParameterDefaultNotes
limit1001 to 500.
offset0Page through with total.
qnoneMatches Embark name, Steam / PSN / Xbox handle, or club tag.
leaguenoneExact league, for example Ruby or Diamond 2.
activitynoneactive, offline-soon, idle.
sortrankrank, rankScore, change24h, name.
includeboardtail 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

GET /v1/players/{embarkId}

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.

GET /v1/players/{embarkId}/history

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.

ParameterDefaultNotes
seasonallFor example s11.
from, toallISO timestamps, inclusive.
limit5000Up 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": { }
}
GET /v1/search?q=

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

GET /v1/seasons

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.

GET /v1/seasons/{season}

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.

GET /v1/cutoff

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

GET /v1/adjustments

Embark's nightly rank score adjustment batches, one entry each, with how many accounts moved and the split of gains to losses.

GET /v1/adjustments/accounts

The individual accounts inside those batches. Filter with at= (a batch timestamp) or player= (an Embark ID).

GET /v1/renames

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.

GET /v1/bans · /v1/unbans

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

GET /v1/tournaments

Tracked community events and majors, with final standings, rosters, prize pools and the badges awarded for them.

GET /v1/badges · /v1/badges/{slug}

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.

GET /v1/ownership/embark/{embarkId} key

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": { }
}
GET /v1/ownership/discord/{discordUserId} key

The reverse: which Embark ID this Discord user has linked. Returns linked: false if they have not linked one.

GET /v1/ownership key

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:

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.