Skip to content

Lobbies

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

Lobbies

Since v0.3.0. steam.lobbies is the curated layer over ISteamMatchmaking: create, join and search lobbies, read and write lobby data, send lobby chat.

Four methods are async: create, join and list await their call result through the dispatch pump, and requestData awaits a callback. Everything else reads or writes the client's local lobby cache, which only holds lobbies this user is in or has just found through list. A cached read for any other lobby answers with an empty string, an empty array or 0, not with an error.

Failures come out in three shapes: a refused async call throws SteamResultError with the EResult attached, a flat call that returns false throws a plain Error, and a rejected join throws an Error naming the EChatRoomEnterResponse, which is a different enum entirely. See Errors.

The Lobbies instance is created lazily and cached on the Steam object. All lobby ids are CSteamID values, so bigint.

create

create(type: number, maxMembers: number): Promise<bigint>

Creates a lobby and joins it. The caller becomes the owner.

Parameter Type Meaning
type number ELobbyType: 0 private, 1 friends-only, 2 public, 3 invisible, 4 private unique.
maxMembers number Member limit including the owner. Steam allows at most 250.

Steam destroys a lobby that has no members left, so a lobby only outlives the process when somebody else joined it.

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

const steam = init({ appId: 480 });
const lobbyId = await steam.lobbies.create(flat.ELobbyType.k_ELobbyTypePublic, 4);
steam.lobbies.setData(lobbyId, 'map', 'de_dust2');
steam.close();

Throws SteamResultError with operation: 'CreateLobby', for example k_EResultLimitExceeded.

join

join(lobbyId: bigint): Promise<void>

Joins an existing lobby. Once it resolves, the lobby data and the member list are in the local cache, so getData, listData and getMembers answer for it right away.

A failed join does not carry an EResult. Steam answers LobbyEnter_t with an EChatRoomEnterResponse, a different enum, so there is no SteamResultError to catch here: the thrown Error names the response instead, for example steamwand: JoinLobby failed: k_EChatRoomEnterResponseFull. The other values you will meet are ...DoesntExist, ...NotAllowed, ...Banned and ...RatelimitExceeded.

const [first] = await steam.lobbies.list({ slotsAvailable: 1 });
if (first) {
  await steam.lobbies.join(first);
  console.log(steam.lobbies.getMembers(first));
}

leave

leave(lobbyId: bigint): void

Leaves a lobby. Steam has no result for this, so it cannot fail from JavaScript. Leaving as the last member destroys the lobby; leaving as the owner hands ownership to another member.

list

list(opts?: LobbySearchOptions): Promise<bigint[]>

Searches for lobbies of this app and returns the matching ids, at most 50, best match first.

The filters apply to this one request and to nothing after it. Steam keeps them on the client until RequestLobbyList goes out and then clears them, so every call needs its own opts. There is no way to set a filter once and reuse it, and a second list() with no opts is unfiltered.

Every match is in the local cache when the promise resolves, so getData and getMemberLimit answer for a lobby you have not joined.

const ids = await steam.lobbies.list({
  stringFilters: { map: 'de_dust2' },
  slotsAvailable: 1,
  maxResults: 10,
});
console.log(ids.map((id) => steam.lobbies.getData(id, 'map')));

getMembers

getMembers(lobbyId: bigint): bigint[]

The Steam ids of everyone currently in the lobby, owner included. Reads the local cache, so it only answers for a lobby this user is in and gives an empty array otherwise.

getOwner

getOwner(lobbyId: bigint): bigint

The owner's Steam id, or 0n when the user is not in that lobby.

setOwner

setOwner(lobbyId: bigint, steamId: bigint): void

Hands ownership to another member. Only the current owner may do this, and the new owner must already be in the lobby. Throws Error: steamwand: SetLobbyOwner returned false (...) otherwise.

getMemberLimit

getMemberLimit(lobbyId: bigint): number

The member limit, or 0 when the lobby is not in the local cache.

setMemberLimit

setMemberLimit(lobbyId: bigint, maxMembers: number): void

Changes the limit, owner only, at most 250. Search results reflect the new limit after the next list.

setType

Since v0.6.0.

setType(lobbyId: bigint, type: number): void

Changes who may find and join the lobby, owner only. type is the same ELobbyType create takes: 0 private, 1 friends-only, 2 public, 3 invisible, 4 private unique. create fixes the type once, and this is how a lobby goes public after the party filled up, or back to private.

steam.lobbies.setType(lobbyId, flat.ELobbyType.k_ELobbyTypePublic);

Throws Error: steamwand: SetLobbyType returned false (...) when the user is not the owner.

setJoinable

Since v0.6.0.

setJoinable(lobbyId: bigint, joinable: boolean): void

Opens or closes the lobby to new members, owner only. A lobby that is not joinable still shows up in list, so this is the "match started" switch, not a way to hide the lobby. Use setType for that.

Throws Error: steamwand: SetLobbyJoinable returned false (...) when the user is not the owner.

inviteUser

inviteUser(lobbyId: bigint, steamId: bigint): void

Invites a user. When they accept, they get a GameLobbyJoinRequested_t callback carrying the lobby id to pass to join. Subscribe to that with steam.on, see Core API.

setGameServer

Since v0.6.0.

setGameServer(lobbyId: bigint, server: { steamId?: bigint; ip?: string; port?: number }): void

Records the game server the members should connect to. This is the handoff at the end of the lobby screen: the owner starts or picks a server, writes it here, and every member gets a LobbyGameCreated_t callback carrying the same values.

Give a steamId for a Steam game server, an ip and port for a plain address, or all three. ip is a dotted quad, which this converts to the host-order uint32 Steam wants. Anything left out is sent as 0, which Steam reads as "not set". Steam returns no result, so this cannot fail from JavaScript.

steam.lobbies.setGameServer(lobbyId, { ip: '192.168.0.10', port: 27015 });

getGameServer

Since v0.6.0.

getGameServer(lobbyId: bigint): { steamId: bigint; ip: string; port: number } | null

The recorded server, or null when the lobby has none yet. A field the owner did not set reads back as 0n, 0.0.0.0 or 0, so check the one your game actually uses rather than the object as a whole.

getData

getData(lobbyId: bigint, key: string): string

One lobby data value. An unset key, or a lobby that is not cached, gives an empty string.

setData

setData(lobbyId: bigint, key: string, value: string): void

Writes one lobby data value, owner only. Keys are capped at 255 UTF-8 bytes and values at 8192. Every member gets a LobbyDataUpdate_t callback, and lobby data is exactly what list filters on, so this is how a lobby advertises its map, mode or version.

Throws Error: steamwand: SetLobbyData returned false (...) when the user is not the owner or the key is too long.

deleteData

deleteData(lobbyId: bigint, key: string): void

Removes one key, owner only. Throws when the user is not the owner or the key was not set.

listData

listData(lobbyId: bigint): Record<string, string>

Every key and value at once. Empty when the lobby is not in the local cache.

const lobbyId = await steam.lobbies.create(flat.ELobbyType.k_ELobbyTypePublic, 4);
steam.lobbies.setData(lobbyId, 'map', 'de_dust2');
console.log(steam.lobbies.listData(lobbyId)); // { map: 'de_dust2' }

requestData

Since v0.6.0.

requestData(lobbyId: bigint): Promise<void>

Pulls one lobby's data into the local cache without joining it. Needed for a lobby this user is not in and did not just find through list, for example one that arrived in a GameLobbyJoinRequested_t invite. Once it resolves, getData and listData answer for that lobby, so an invite can show the map and the mode before the player commits.

steam.on('GameLobbyJoinRequested_t', async (e) => {
  await steam.lobbies.requestData(e.m_steamIDLobby);
  console.log(steam.lobbies.getData(e.m_steamIDLobby, 'map'));
});

Steam answers with a LobbyDataUpdate_t whose member id equals the lobby id. Throws Error: steamwand: RequestLobbyData failed: lobby <id> no longer exists when that answer clears its success flag, and Error: steamwand: RequestLobbyData returned false (...) when Steam refuses the request outright.

getMemberData

getMemberData(lobbyId: bigint, steamId: bigint, key: string): string

One value a member set on themselves, for example their chosen character or their ready flag. Empty string when the key is unset.

setMemberData

setMemberData(lobbyId: bigint, key: string, value: string): void

Writes one value on this user's own membership. A member can only write their own data, which is why there is no Steam id parameter. Every member gets a LobbyDataUpdate_t callback. Steam has no result for this, so it cannot fail from JavaScript.

sendChat

sendChat(lobbyId: bigint, message: string): void

Sends a message to every member of the lobby, this user included. The text goes out as UTF-8 with a terminating NUL, which is what onChat and Valve's own samples expect. Max 4096 UTF-8 bytes including the terminator.

Throws Error: steamwand: SendLobbyChatMsg returned false (...) when the user is not in that lobby or the message is too long.

onChat

onChat(lobbyId: bigint, listener: (message: LobbyChatMessage) => void): () => void

Subscribes to the chat of one lobby and returns an unsubscribe function. Messages for other lobbies are filtered out, so one listener per lobby is enough, and this user's own messages arrive too.

The message text is not in the callback. Steam only says "entry number N is ready", so the payload is fetched with GetLobbyChatEntry inside the same pump frame, before Steam drops it. The trailing NUL is stripped again, so text round trips through sendChat unchanged.

const off = steam.lobbies.onChat(lobbyId, (m) =>
  console.log(m.senderSteamId, m.message),
);
steam.lobbies.sendChat(lobbyId, 'hello');
// later: off();

Calling the returned function more than once is harmless.

onDataChange

Since v0.6.0.

onDataChange(lobbyId: bigint, listener: (change: { memberSteamId: bigint | null }) => void): () => void

Subscribes to the data changes of one lobby and returns an unsubscribe function. Updates for other lobbies are filtered out.

The callback says only that something changed, not what, so read the new values with getData, listData or getMemberData inside the listener. memberSteamId is null when the lobby's own data changed and the member's Steam id when that member changed their own data. Steam signals the first case by repeating the lobby id in the member field; this layer turns that into null so there is no bogus Steam id to filter out.

const off = steam.lobbies.onDataChange(lobbyId, ({ memberSteamId }) => {
  if (memberSteamId === null) console.log(steam.lobbies.listData(lobbyId));
  else console.log(steam.lobbies.getMemberData(lobbyId, memberSteamId, 'ready'));
});

onMemberChange

Since v0.6.0.

onMemberChange(lobbyId: bigint, listener: (change: LobbyMemberChange) => void): () => void

Subscribes to the membership changes of one lobby and returns an unsubscribe function. This is how a lobby screen learns that somebody joined, left, dropped or was thrown out.

Steam packs several EChatMemberStateChange bits into one callback, so a member who timed out arrives as both left and disconnected. The listener runs once per set bit, in the order entered, left, disconnected, kicked, banned.

const off = steam.lobbies.onMemberChange(lobbyId, (c) => {
  console.log(c.steamId, c.change, steam.lobbies.getMembers(lobbyId).length);
});

Types

LobbySearchOptions

Filters for one list call. Every field is optional, and every field applies to that request only.

Field Type Meaning
stringFilters Record<string, string> Lobby data keys that must equal these values. Compared as strings, with k_ELobbyComparisonEqual.
numberFilters Record<string, number> Lobby data keys that must equal these numbers. Compared as 32-bit integers.
slotsAvailable number Only lobbies with at least this many open slots.
distance number ELobbyDistanceFilter: 0 close, 1 default, 2 far, 3 worldwide.
maxResults number Stop searching after this many matches. Steam returns at most 50 either way.

Only equality comparisons are exposed. The other ELobbyComparison members and the near-value and compatible-members filters are on the raw interface, see below.

LobbyChatMessage

Handed to an onChat listener.

Field Type Meaning
senderSteamId bigint Steam id of the member who sent it.
message string The text, decoded as UTF-8, terminator removed.

LobbyMemberChange

Since v0.6.0. Handed to an onMemberChange listener.

Field Type Meaning
steamId bigint Steam id of the member this happened to.
bySteamId bigint Steam id of whoever caused it. The same as steamId unless somebody was kicked or banned.
change 'entered' | 'left' | 'disconnected' | 'kicked' | 'banned' The EChatMemberStateChange bit that was set.

Errors

Shape When
SteamResultError CreateLobby completed with a non-OK EResult. It is the only method here that reports through an EResult.
Error: steamwand: JoinLobby failed: <EChatRoomEnterResponse name> The join was refused. Not an EResult, so not a SteamResultError.
Error: steamwand: <call> returned false (invalid handle or argument?) SetLobbyData, DeleteLobbyData, SetLobbyOwner, SetLobbyMemberLimit, SetLobbyType, SetLobbyJoinable, InviteUserToLobby, RequestLobbyData or SendLobbyChatMsg returned false. Usually: not the owner, not in the lobby, or a value over its cap.
Error: steamwand: RequestLobbyData failed: lobby <id> no longer exists requestData got its answer with the success flag clear.
SteamApiCallError create, join or list never produced a usable result, or completed carrying a different callback struct. See How It Works.

leave, setMemberData and setGameServer have no failure path at all, because Steam returns nothing from them.

What this layer does not do

The rest of ISteamMatchmaking is on the raw generated steam.matchmaking:

  • SetLinkedLobby, which chains a second lobby to this one.
  • The LobbyGameCreated_t callback that setGameServer raises on every member.
  • The filters list does not expose: AddRequestLobbyListNearValueFilter, AddRequestLobbyListCompatibleMembersFilter, and the non-equality ELobbyComparison members.
  • The favorite-games list: GetFavoriteGameCount, GetFavoriteGame, AddFavoriteGame, RemoveFavoriteGame.
  • Server browsing, which is a separate interface (ISteamMatchmakingServers) and is not wrapped at all.

GameLobbyJoinRequested_t and LobbyGameCreated_t are not wrapped either. Subscribe to them with steam.on, which decodes them from the generated layouts. Flat API explains the calling convention, and Core API covers steam.on.

Clone this wiki locally