Skip to content
Joël Deffner edited this page Sep 4, 2026 · 2 revisions

Stats

Since v0.3.0. steam.stats is the curated layer over ISteamUserStats: the achievements and the per-user stats of the logged-in account.

Steam loads the current user's stats during init, so the read methods answer right away. Almost everything here is a local read against the client's copy. unlock, clear, store and resetAll are the only synchronous methods that send anything to Steam, and five methods are async: getNumberOfCurrentPlayers, getGlobalPercentages, requestUserStats, requestGlobalStats and achievementIcon.

Failures come out in two shapes. A flat call that returns false, which for this interface almost always means the API name is not one this app defines, throws a plain Error prefixed with steamwand:. An async call that Steam refuses throws SteamResultError with the EResult attached. See Errors.

The Stats instance is created lazily and cached on the Steam object. The leaderboard half of the same interface lives on Leaderboards.

isAchieved

isAchieved(name: string): boolean

Whether the current user unlocked the achievement. name is the API name from the partner site, not the display name.

if (!steam.stats.isAchieved('ACH_WIN_ONE_GAME')) {
  steam.stats.unlock('ACH_WIN_ONE_GAME');
}

Throws Error: steamwand: GetAchievement returned false (...) when the app has no achievement with that API name.

getAchievement

getAchievement(name: string): AchievementState

The same read plus the unlock time. unlockTime is Unix seconds while the achievement is unlocked and null while it is locked, so there is no epoch-zero date to filter out.

const { achieved, unlockTime } = steam.stats.getAchievement('ACH_WIN_ONE_GAME');
if (achieved) console.log(new Date(unlockTime! * 1000));

Throws the same Error as isAchieved for an unknown API name.

getDisplay

getDisplay(name: string): AchievementDisplay

The display name, the description and the hidden flag, in the Steam client language. The text comes from the achievement configuration on the partner site, so a field that was never configured reads back as an empty string, and an unknown API name gives three empty fields rather than an error. A hidden achievement has an empty description until the user unlocks it.

achievementIcon

Since v0.6.0.

achievementIcon(name: string): Promise<number | null>

The Steam image handle of the achievement's icon, or null when the achievement has none configured. Decode the pixels with system.image.

Steam either has the icon cached, in which case the promise resolves at once, or it starts a download and answers with UserAchievementIconFetched_t. Both paths end here, so there is no handle of 0 to check for. The icon is the locked or the unlocked one, whichever matches the current state, so read it again after unlock.

const handle = await steam.stats.achievementIcon('ACH_WIN_ONE_GAME');
const icon = handle === null ? null : steam.system.image(handle);
console.log(icon?.width, icon?.height);

An API name this app does not define never gets an answer. Steam does not say "no such achievement" here, so the promise waits until the session closes. Check the name against listAchievements first.

listAchievements

listAchievements(): string[]

The API names of every achievement this app defines, in partner-site order. Steam knows the list only once it has the app's stats schema, so an empty array right after init means the schema has not arrived yet, not that the app has no achievements.

for (const name of steam.stats.listAchievements()) {
  console.log(name, steam.stats.getDisplay(name).name, steam.stats.isAchieved(name));
}

unlock

unlock(name: string): void

Sets the achievement and stores immediately, because Steam draws the unlock toast on the store. Unlocking an already unlocked achievement does nothing. Unlike the stat setters, this one needs no separate store.

Throws for an unknown API name, and for a failed store (naming StoreStats).

clear

clear(name: string): void

Locks the achievement again and stores. Meant for testing; a released game normally never clears an achievement.

indicateProgress

indicateProgress(name: string, current: number, max: number): void

Draws the progress toast ("30 of 100 kills") and nothing else. It does not unlock the achievement and it does not record the progress anywhere: keep the count in a stat and call unlock yourself when it reaches max. Steam ignores the call when current is not above the last value it showed.

const kills = steam.stats.getInt('kills') + 1;
steam.stats.setInt('kills', kills);
steam.stats.store();
if (kills < 100) steam.stats.indicateProgress('ACH_100_KILLS', kills, 100);
else steam.stats.unlock('ACH_100_KILLS');

getProgressLimits

Since v0.6.0.

getProgressLimits(name: string): { min: number; max: number } | null

The progress range the partner site configured for the achievement, or null for an achievement with no progress configured. Saves hard-coding the "of 100" in indicateProgress, so the toast stays right when the configuration changes.

const limits = steam.stats.getProgressLimits('ACH_100_KILLS');
if (limits) {
  steam.stats.indicateProgress('ACH_100_KILLS', steam.stats.getInt('kills'), limits.max);
}

getInt

getInt(name: string): number

An INT stat of the current user. A stat the user never set reads back as 0. Throws for an API name the app does not define as an INT stat.

getFloat

getFloat(name: string): number

A FLOAT stat, or an AVGRATE stat, which reads back as the current average.

setInt

setInt(name: string, value: number): void

The value stays local until store runs. That is deliberate: set every stat that changed, then store once. Steam rejects a value above the maximum the partner site configured, which surfaces at store, not here.

setFloat

setFloat(name: string, value: number): void

Same, for a FLOAT stat. Local until store.

updateAvgRate

updateAvgRate(name: string, sessionCount: number, sessionLength: number): void

Feeds one session into an AVGRATE stat, for example kills per hour. Steam keeps the running average itself; read it back with getFloat. sessionLength is in the unit the stat is configured with, usually seconds. Local until store.

steam.stats.updateAvgRate('kills_per_hour', 42, 1800);
steam.stats.store();

store

store(): void

Sends every pending stat change to Steam. Nothing the setters wrote is persisted until this runs, so call it at a natural break: end of a level, end of a match, shutdown. It returns as soon as Steam accepted the batch, and the server round trip finishes in the background.

Throws Error: steamwand: StoreStats returned false (...) when Steam refuses the batch, which means it is not logged on, or a value is above its configured maximum.

resetAll

resetAll(alsoAchievements?: boolean): void

Resets every stat of the current user, and every achievement too when alsoAchievements is true (default false). It stores by itself and it cannot be undone. Meant for testing.

getNumberOfCurrentPlayers

getNumberOfCurrentPlayers(): Promise<number>

How many people are playing this app right now.

console.log(await steam.stats.getNumberOfCurrentPlayers(), 'players online');

Throws Error: steamwand: GetNumberOfCurrentPlayers failed (is Steam online?) when Steam sets the failure flag on the result, which is what an offline client does.

getGlobalPercentages

getGlobalPercentages(): Promise<Record<string, number>>

For every achievement of this app, the share of players who unlocked it, 0 to 100. One round trip (RequestGlobalAchievementPercentages) fills Steam's local cache, then every percentage is read out of it. An achievement Steam has no data for is absent from the record, so read it with ?? 0.

const percent = await steam.stats.getGlobalPercentages();
console.log(percent['ACH_WIN_ONE_GAME']?.toFixed(1), '% of players');

Throws SteamResultError with operation: 'RequestGlobalAchievementPercentages' on a non-OK result, for example k_EResultFail while the client is offline.

requestUserStats

requestUserStats(steamId: bigint): Promise<void>

Downloads another user's stats and achievements into Steam's local cache. The current user's are already there, so this is only for other people.

The method returns nothing on purpose. Once it resolves, the values are in the cache and you read them with getUserAchievement, getUserInt and getUserFloat.

const friend = 76561197960287930n;
await steam.stats.requestUserStats(friend);
console.log(steam.stats.getUserInt(friend, 'kills'));

Throws SteamResultError with operation: 'RequestUserStats', typically k_EResultFail when the other user's profile is private.

getUserAchievement

Since v0.6.0.

getUserAchievement(steamId: bigint, name: string): AchievementState

Another user's achievement with its unlock time, in the same AchievementState shape as getAchievement.

requestUserStats must have resolved for that user first. This is a local read against the cache it filled, so a user who is not in the cache throws the same plain Error as an unknown API name.

await steam.stats.requestUserStats(friend);
console.log(steam.stats.getUserAchievement(friend, 'ACH_WIN_ONE_GAME').achieved);

getUserInt

Since v0.6.0.

getUserInt(steamId: bigint, name: string): number

Another user's INT stat, 0 when they never set it. requestUserStats must have resolved for that user first.

getUserFloat

Since v0.6.0.

getUserFloat(steamId: bigint, name: string): number

Another user's FLOAT or AVGRATE stat, the latter as their current average. requestUserStats must have resolved for that user first.

requestGlobalStats

Since v0.6.0.

requestGlobalStats(historyDays?: number): Promise<void>

Downloads this app's aggregated global stats into Steam's local cache. Global stats are the sums over every player, and only a stat the partner site marks as aggregated has one. Nothing in this group reads back before this resolves, so it is always the first call.

historyDays is how many days of daily history to fetch as well, at most 60, default 0 for the totals only. getGlobalIntHistory reads no more days than were requested here.

await steam.stats.requestGlobalStats(7);
console.log(steam.stats.getGlobalInt('kills_total'));
console.log(steam.stats.getGlobalIntHistory('kills_total', 7));

Throws SteamResultError with operation: 'RequestGlobalStats', for example k_EResultInvalidState when the app aggregates no stat at all.

getGlobalInt

Since v0.6.0.

getGlobalInt(name: string): bigint

The global total of an INT stat. It sums over every player, so it is 64-bit and comes back as a bigint. requestGlobalStats must have resolved first.

getGlobalDouble

Since v0.6.0.

getGlobalDouble(name: string): number

The global total of a FLOAT stat. requestGlobalStats must have resolved first.

getGlobalIntHistory

Since v0.6.0.

getGlobalIntHistory(name: string, days: number): bigint[]

The daily history of an INT global stat, one total per day, today first. requestGlobalStats must have resolved with at least this many historyDays, or the history is not in the cache.

The array is shorter than days when Steam has less history, and empty when it has none, so read it by length rather than by index.

await steam.stats.requestGlobalStats(7);
const [today, yesterday] = steam.stats.getGlobalIntHistory('kills_total', 7);

Types

AchievementState

Returned by getAchievement and getUserAchievement.

Field Type Meaning
achieved boolean True once the user unlocked it.
unlockTime number | null Unix seconds, or null while the achievement is locked.

AchievementDisplay

Returned by getDisplay. All three come from GetAchievementDisplayAttribute.

Field Type Attribute Meaning
name string name Localized display name. Empty when unconfigured or unknown.
description string desc Localized description. Empty while a hidden achievement is locked.
hidden boolean hidden True when Steam hides the achievement until it unlocks. The raw attribute is the string '1'.

Errors

Shape When
Error: steamwand: <call> returned false (invalid handle or argument?) A flat call returned false. For this interface that is almost always an API name the app does not define, or a name used with the wrong type (getInt on a FLOAT stat). <call> is the flat name: GetAchievement, SetStatInt32, StoreStats, and so on.
Error: steamwand: GetNumberOfCurrentPlayers failed (is Steam online?) The player-count result came back with its success flag clear.
SteamResultError RequestGlobalAchievementPercentages, RequestUserStats or RequestGlobalStats completed with a non-OK EResult. operation and result are on the error.
SteamApiCallError One of the four call-result methods never produced a usable result, or completed carrying a different callback struct. See How It Works.

There is no separate "stat does not exist" error class. A wrong API name and a wrong type both surface as the same plain Error, so check the name against listAchievements or the partner site when one appears.

What this layer does not do

The rest of ISteamUserStats is on the raw generated steam.userStats:

  • Leaderboards. Those are wrapped separately, on Leaderboards.
  • GetGlobalStatHistoryDouble, the FLOAT counterpart of getGlobalIntHistory.
  • GetMostAchievedAchievementInfo and GetNextMostAchievedAchievementInfo, the iterator form of the global percentages.
  • GetAchievementProgressLimitsFloat, the FLOAT counterpart of getProgressLimits.

Those take raw Buffer out params and return flat booleans. Flat API explains the calling convention.

Clone this wiki locally