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

Social

Since v0.5.0. steam.social is the curated layer over ISteamFriends: the local persona, the friend list, avatars, and rich presence. It is called social because the generated ISteamFriends class already owns steam.friends.

Nothing here is async. Every read answers from the Steam client's local cache, so the whole layer is synchronous, and the two things that do take time (persona data for a stranger, avatar pixels) are handled by asking Steam to fetch and reading again from a callback. The client only caches persona data for users it has a reason to know about: friends, people in the same lobby, and anybody you asked for with requestUserInformation. For everyone else the reads answer with an empty string or a zero, not with an error.

The overlay half of the same interface lives on Overlay; clans, chat and coplay stay on the raw steam.friends. The Social instance is created lazily and cached on the Steam object. Steam ids are bigint.

The local persona

personaName(): string
personaState(): number

The local user's display name, the one other players see, and their EPersonaState: 0 offline, 1 online, 2 busy, 3 away, 4 snooze, 7 invisible.

listFriends

listFriends(flags?: number): Friend[]

The friend list with names, states and relationships, in Steam's order.

flags is an EFriendFlags bit field and defaults to k_EFriendFlagAll, which is everything the client knows about, blocked users and clan members included. Pass k_EFriendFlagImmediate for the plain friend list.

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

const steam = init({ appId: 480 });
const online = steam.social
  .listFriends(flat.EFriendFlags.k_EFriendFlagImmediate)
  .filter((f) => f.state !== flat.EPersonaState.k_EPersonaStateOffline);
console.log(online.map((f) => f.name));
steam.close();

friendName and friendState

friendName(steamId: bigint): string
friendState(steamId: bigint): number

One user's persona name and EPersonaState. An uncached user reads back as an empty name and offline (0), and offline is indistinguishable from actually being offline, so call requestUserInformation first when that matters.

friendGame

Since v0.6.0.

friendGame(steamId: bigint): FriendGame | null

What a user is playing right now, or null when they are in no game or the client has no data for them.

Steam answers with a CGameID, which packs the app id into its low 24 bits; appId is that app id, already unpacked. A user on a game server also reports the server address as a dotted quad and its two ports, and a user in a lobby reports a lobbyId you can pass straight to lobbies.join. A single player session reports an empty ip, zero ports and a null lobby.

for (const friend of steam.social.listFriends()) {
  const game = steam.social.friendGame(friend.steamId);
  if (game) console.log(friend.name, 'is in', game.appId);
}

listFriendsInGame

Since v0.6.0.

listFriendsInGame(appId?: number): (Friend & { game: FriendGame })[]

The immediate friends who are playing one app right now, in Steam's friend list order. appId defaults to the running app, which makes this the "who of my friends is in this game" list an invite UI needs.

Each entry is a Friend with its FriendGame attached, so an invite needs no second call:

for (const friend of steam.social.listFriendsInGame()) {
  if (friend.game.lobbyId) await steam.lobbies.join(friend.game.lobbyId);
}

Only k_EFriendFlagImmediate friends are considered. Clan members and blocked users never show up here, whatever they are playing.

friendLevel, nickname and hasFriend

Since v0.6.0.

friendLevel(steamId: bigint): number
nickname(steamId: bigint): string | null
hasFriend(steamId: bigint, flags?: number): boolean

friendLevel is a user's Steam level, or 0 while the client has no data for them. nickname is the private nickname the local user gave that user, or null when there is none: nicknames are local to this account, nobody else sees them, and showing one instead of the persona name is what the Steam client itself does. hasFriend checks the relationship against an EFriendFlags bit field, defaulting to k_EFriendFlagImmediate, the plain friend list.

const other = 76561197960287930n;
const label = steam.social.nickname(other) ?? steam.social.friendName(other);
console.log(label, steam.social.friendLevel(other), steam.social.hasFriend(other));

requestUserInformation

requestUserInformation(steamId: bigint, nameOnly?: boolean): boolean

Asks Steam to fetch persona data for a user who is not a friend. nameOnly (default false) fetches only the name and the avatar, which is faster.

Returns true when Steam started a fetch and false when the data was already cached. A false means no callback follows. The data arrives on onPersonaStateChange; read it back with friendName or friendState, or with avatar.

const other = 76561197960287930n;
if (steam.social.requestUserInformation(other, true)) {
  steam.social.onPersonaStateChange((e) => {
    if (e.steamId === other) console.log(steam.social.friendName(other));
  });
}

onPersonaStateChange

onPersonaStateChange(listener: (change: PersonaStateChange) => void): () => void

Subscribes to persona changes of any user the client tracks, and returns an unsubscribe function. This fires often and for many users, the local one included, so filter on steamId before doing work.

avatar

avatar(steamId: bigint, size?: AvatarSize): Avatar | null

A user's avatar as raw RGBA pixels. size is 'small' (32x32), 'medium' (64x64, the default) or 'large' (184x184), but read the reported width and height rather than assuming them.

Steam hands out an image handle first and loads the pixels afterwards, so the first call for an uncached user returns null. Subscribe to AvatarImageLoaded_t with steam.on and call this again from the listener. For a non-friend, call requestUserInformation first to start the fetch at all.

const me = steam.steamId();
const off = steam.on('AvatarImageLoaded_t', (e) => {
  if (e.m_steamID !== me) return;
  const a = steam.social.avatar(me, 'large');
  if (a) console.log(a.width, a.height, a.rgba.length);
  off();
});

Throws Error: steamwand: GetImageSize returned false (...) when Steam refuses the handle it just handed out.

setRichPresence

setRichPresence(key: string, value: string): void

Sets one rich presence key on the local user. Friends read it with getRichPresence, and Steam shows the value of the status key in the friend list. The connect key is the one Steam turns into a "Join game" option, which arrives on the other side as onGameRichPresenceJoinRequested.

Steam allows 20 keys, 64 bytes per key and 256 bytes per value. An empty value removes the key.

steam.social.setRichPresence('status', 'In the lobby');
steam.social.setRichPresence('connect', '+connect_lobby 109775242724');

Throws Error: steamwand: SetRichPresence returned false (...) when a key or value is too long, or the 20 key limit is reached.

clearRichPresence

clearRichPresence(): void

Removes every rich presence key of the local user. Steam has no result for this, so it cannot fail from JavaScript.

getRichPresence and listRichPresence

getRichPresence(steamId: bigint, key: string): string
listRichPresence(steamId: bigint): Record<string, string>

One rich presence key of a user, or all of them at once on a null-prototype object so a key named toString is safe. Only friends running the same app have rich presence, and only once the client cached it. Pass the local Steam id to read back what setRichPresence wrote.

onGameRichPresenceJoinRequested

onGameRichPresenceJoinRequested(
  listener: (request: RichPresenceJoinRequest) => void,
): () => void

Fires when a friend clicks "Join game" while this app is already running. The connect string is whatever that friend put in their connect key. When the app is not running, Steam passes the same string on the command line as +connect <value> instead, so handle both paths.

onGameLobbyJoinRequested

onGameLobbyJoinRequested(listener: (request: LobbyJoinRequest) => void): () => void

Fires when the user accepts a lobby invite while this app is running. Pass lobbyId straight to lobbies.join.

steam.social.onGameLobbyJoinRequested(async (r) => {
  await steam.lobbies.join(r.lobbyId);
});

inviteToGame and setPlayedWith

Since v0.6.0.

inviteToGame(steamId: bigint, connectString: string): void
setPlayedWith(steamId: bigint): void

inviteToGame sends a friend an invite into this user's game, shown in their Steam chat. If they already run the app it arrives as onGameRichPresenceJoinRequested carrying this connect string; if not, Steam launches the app and passes the string on the command line, where system.launchCommandLine reads it. The string is capped at 256 UTF-8 bytes.

setPlayedWith puts a user in the local user's "Recently played with" list, which is where a player goes to add a stranger from the last match as a friend. Call it once per other player at the end of a session. Steam has no result for it, so it cannot fail from JavaScript.

const lobbyId = await steam.lobbies.create(flat.ELobbyType.k_ELobbyTypeFriendsOnly, 4);
steam.social.inviteToGame(friendId, `+connect_lobby ${lobbyId}`);
// at the end of the match:
steam.social.setPlayedWith(friendId);

Throws Error: steamwand: InviteUserToGame returned false (...) when the connect string is over its byte cap or the user cannot be invited.

Types

Friend

One entry from listFriends.

Field Type Meaning
steamId bigint Steam id of the friend.
name string Persona name, empty while Steam has not cached it.
state number EPersonaState.
relationship number EFriendRelationship: 0 none, 1 blocked, 3 friend.

FriendGame

Since v0.6.0. Returned by friendGame, and attached as game to every entry of listFriendsInGame.

Field Type Meaning
appId number App id of the game, from the low 24 bits of the CGameID.
gameId bigint The full CGameID, which also encodes the id type and the mod id.
lobbyId bigint | null Lobby the user is in, ready for steam.lobbies.join, or null for none.
ip string Game server address as a dotted quad, empty when there is no server.
port number Game server port, 0 when there is no server.
queryPort number Game server query port, 0 when there is no server.

Avatar

Returned by avatar. AvatarSize is 'small' | 'medium' | 'large'.

Field Type Meaning
width number Image width in pixels.
height number Image height in pixels.
rgba Buffer Raw pixels, 4 bytes each in RGBA order, width * height * 4 long.

PersonaStateChange

Field Type Meaning
steamId bigint The user whose persona changed.
changeFlags number EPersonaChange bit field: which parts changed.

RichPresenceJoinRequest

Field Type Meaning
steamId bigint The friend who sent the invite.
connect string That friend's connect key, max 256 bytes.

LobbyJoinRequest

Field Type Meaning
lobbyId bigint Lobby to pass to steam.lobbies.join.
steamId bigint The friend who invited, or a clan id for a group invite.

Errors

Shape When
Error: steamwand: SetRichPresence returned false (invalid handle or argument?) A key or value is over its byte cap, or the 20 key limit is reached.
Error: steamwand: InviteUserToGame returned false (...) inviteToGame got a connect string over 256 bytes, or a user Steam will not deliver an invite to.
Error: steamwand: GetImageSize returned false (...), GetImageRGBA returned false (...) avatar got a handle from Steam that Steam then refused.

Nothing here throws SteamResultError, because nothing here waits for a call result.

What this layer does not do

The rest of ISteamFriends is on the raw generated steam.friends:

  • The overlay calls, wrapped separately on Overlay.
  • Clans: GetClanCount, GetClanName, GetClanOfficerCount, and the clan chat calls.
  • Coplay: GetCoplayFriendCount and GetFriendCoplayGame.
  • SetPersonaName, SetListenForFriendsMessages and the friend messaging calls.
  • The friend groups: GetFriendsGroupCount and its index reads.

Flat API explains the calling convention.

Next: Overlay, the dialog half of the same interface.

Clone this wiki locally