Skip to content

Leaderboards

Joël Deffner edited this page Sep 1, 2026 · 1 revision

Leaderboards

Since v0.3.0. steam.leaderboards is the curated layer over the leaderboard half of ISteamUserStats: find or create a leaderboard, upload a score, download entries, attach a replay.

Every method is async and awaits its call result through the dispatch pump, see How It Works. Failures are thrown rather than reported as a flag on a struct: a non-OK EResult becomes a SteamResultError, and Steam's own boolean failure flags become plain Errors. See Errors.

Every method except find and findOrCreate takes the handle (SteamLeaderboard_t), not the name, so keep the LeaderboardInfo around for as long as you use the leaderboard. Handles are 64-bit, so bigint, like every other Steam id here.

The achievements and per-user stats on the same interface are on Stats.

find

find(name: string): Promise<LeaderboardInfo | null>

Looks a leaderboard up by name. The name is case sensitive and capped at 128 UTF-8 bytes.

Returns null when the app has no leaderboard with that name. That is the difference to findOrCreate, which throws instead. A missing leaderboard is a normal answer here, so treat the null as data, not as an error.

const board = await steam.leaderboards.find('Fastest Lap');
if (board) console.log(board.handle, board.entryCount);

Throws SteamApiCallError only if the call itself could not be completed.

findOrCreate

findOrCreate(
  name: string,
  sortMethod: number,
  displayType: number,
): Promise<LeaderboardInfo>

The same lookup, but it creates the leaderboard when the app has none.

Parameter Type Meaning
name string Leaderboard name, max 128 UTF-8 bytes, case sensitive.
sortMethod number ELeaderboardSortMethod: 1 ascending (lowest score is best), 2 descending.
displayType number ELeaderboardDisplayType: 1 numeric, 2 seconds, 3 milliseconds.

The last two arguments only apply to a leaderboard this call creates. An existing leaderboard keeps whatever it was created with, and passing different values here does not change it. Read the real settings back from the returned sortMethod and displayType.

import { init, flat } from 'steamwand.js';

const steam = init({ appId: 480 });
const board = await steam.leaderboards.findOrCreate(
  'Fastest Lap',
  flat.ELeaderboardSortMethod.k_ELeaderboardSortMethodAscending,
  flat.ELeaderboardDisplayType.k_ELeaderboardDisplayTypeTimeMilliSeconds,
);
steam.close();

Throws Error: steamwand: FindOrCreateLeaderboard could not find or create "<name>" when Steam did neither.

uploadScore

uploadScore(
  handle: bigint,
  score: number,
  opts?: { method?: number; details?: number[] },
): Promise<ScoreUploadResult>

Uploads the current user's score.

Parameter Type Default Meaning
handle bigint none From find or findOrCreate.
score number none Steam stores a 32-bit signed integer, so scale times and floats yourself.
opts.method number k_ELeaderboardUploadScoreMethodKeepBest ELeaderboardUploadScoreMethod.
opts.details number[] none Game defined details stored with the entry, at most 64 int32 values.

With the default keep-best method Steam drops the upload when the user already has a better score and reports that as scoreChanged: false. That is not an error, and newGlobalRank still holds the rank the old score has. Force-update (k_ELeaderboardUploadScoreMethodForceUpdate) always overwrites, which is what a "reset my time" button needs.

const { scoreChanged, newGlobalRank } = await steam.leaderboards.uploadScore(
  board.handle,
  91_240,
);
console.log(scoreChanged ? `now rank ${newGlobalRank}` : 'kept the old score');

Throws Error: steamwand: UploadLeaderboardScore failed (leaderboard handle or score rejected) when Steam clears the success flag on the result.

downloadEntries

downloadEntries(handle: bigint, opts?: DownloadOptions): Promise<LeaderboardEntry[]>

Downloads a range of entries, best first.

How the range is read depends on dataRequest, and this is the part that surprises people. Global takes absolute 1-based ranks, so 1 to 10 is the top ten. Around-user takes offsets from the user's own rank, so -4 to 5 is a ten entry window centred on them, and 1 to 10 would be the ten people behind them. Friends ignores the range entirely and returns every friend with an entry.

import { init, flat } from 'steamwand.js';

const steam = init({ appId: 480 });
const board = await steam.leaderboards.findOrCreate('Fastest Lap', 1, 3);

const top = await steam.leaderboards.downloadEntries(board.handle, {
  rangeStart: 1,
  rangeEnd: 10,
});
const nearMe = await steam.leaderboards.downloadEntries(board.handle, {
  dataRequest: flat.ELeaderboardDataRequest.k_ELeaderboardDataRequestGlobalAroundUser,
  rangeStart: -4,
  rangeEnd: 5,
});
for (const e of top) console.log(e.globalRank, e.steamId, e.score);
steam.close();

Steam can return fewer entries than the range asked for, near the ends of the board or when the user has no entry of their own.

downloadEntriesForUsers

downloadEntriesForUsers(
  handle: bigint,
  steamIds: bigint[],
  maxDetails?: number,
): Promise<LeaderboardEntry[]>

The entries of a named set of users, for example everyone in a lobby. Steam caps the request at 100 users. Users without an entry on this leaderboard are left out, so the result can be shorter than steamIds, and the order is by rank, not by the order you passed the ids in.

const members = steam.lobbies.getMembers(lobbyId);
const scores = await steam.leaderboards.downloadEntriesForUsers(board.handle, members);

Throws Error: steamwand: downloadEntriesForUsers needs at least one Steam id for an empty array, before any native call.

attachUgc

attachUgc(handle: bigint, ugcHandle: bigint): Promise<void>

Attaches a piece of UGC, usually a replay file, to the current user's entry on this leaderboard. The user must already have an entry, and one entry holds one UGC handle, so a second call replaces the first.

ugcHandle is a UGCHandle_t, which you get by sharing a cloud file with steam.remoteStorage.FileShare. That call is not part of the curated cloud layer, see Cloud. Entries you download carry the handle back as ugcHandle.

Throws SteamResultError with operation: 'AttachLeaderboardUGC', typically k_EResultFail when the user has no entry on this leaderboard.

Types

LeaderboardInfo

Returned by find and findOrCreate. The four properties beside the handle are local reads against the client's copy, so they cost no round trip.

Field Type Meaning
handle bigint SteamLeaderboard_t. Pass it to every other method.
name string Leaderboard name as Steam has it.
entryCount number Entries on the leaderboard right now.
sortMethod number ELeaderboardSortMethod: 0 none, 1 ascending, 2 descending.
displayType number ELeaderboardDisplayType: 0 none, 1 numeric, 2 seconds, 3 milliseconds.

ScoreUploadResult

Returned by uploadScore.

Field Type Meaning
scoreChanged boolean True when the score replaced the previous one. False when keep-best kept the old score.
newGlobalRank number Rank after the upload, 1-based.
previousGlobalRank number Rank before it, 1-based, or 0 when the user had no entry yet.

LeaderboardEntry

One row from downloadEntries or downloadEntriesForUsers.

Field Type Meaning
steamId bigint Steam id of the user who set the score.
globalRank number Rank on the leaderboard, 1-based.
score number The score. Always a 32-bit signed integer.
details number[] Game defined details, at most maxDetails of them. Empty when none were requested or stored.
ugcHandle bigint UGCHandle_t of the attached replay, or k_UGCHandleInvalid when the entry has none.

The entry set handle Steam returns is only valid until the next download, so this layer decodes every row before it resolves. There is no cursor to hold on to and nothing to release.

DownloadOptions

Every field is optional.

Field Type Default Meaning
dataRequest number k_ELeaderboardDataRequestGlobal ELeaderboardDataRequest: 0 global, 1 around user, 2 friends.
rangeStart number 1 First entry. Absolute rank for global, offset from the user's rank for around-user, ignored for friends.
rangeEnd number 10 Last entry, read the same way.
maxDetails number 0 Details to read per entry. Steam stores at most 64. At 0 no details buffer is allocated at all.

Errors

Shape When
SteamResultError AttachLeaderboardUGC completed with a non-OK EResult. It is the only method here that reports through an EResult.
Error: steamwand: FindOrCreateLeaderboard could not find or create "<name>" Steam cleared the found flag on the result.
Error: steamwand: UploadLeaderboardScore failed (...) Steam cleared the success flag on the upload result.
Error: steamwand: downloadEntriesForUsers needs at least one Steam id An empty steamIds array.
Error: steamwand: GetDownloadedLeaderboardEntry returned false (...) A row inside the count Steam reported could not be read.
SteamApiCallError A call never produced a usable result, or completed carrying a different callback struct.

Note the shape of the two "failed" errors: Steam reports find and upload failures as a boolean on the result struct, not as an EResult, so there is no result code to attach and no SteamResultError to catch.

What this layer does not do

Six methods cover the whole leaderboard surface of ISteamUserStats, so little is left over. What this layer hides rather than drops:

  • The raw entry-set handle from LeaderboardScoresDownloaded_t, and the GetDownloadedLeaderboardEntry loop over it. Decoding happens before the promise resolves, because the handle dies at the next download.
  • The four separate property reads behind LeaderboardInfo (GetLeaderboardName, GetLeaderboardEntryCount, GetLeaderboardSortMethod, GetLeaderboardDisplayType).

Both are on the raw generated steam.userStats if you want them. Await the call handles through steam.dispatch, or use steam.async.userStats, which returns the undecoded result structs as promises. Flat API explains the calling convention, and achievements and stats are on Stats.

Clone this wiki locally