Skip to content

Flat API

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

Flat API

The flat layer is the whole Steamworks C API, generated. scripts/generate.ts reads Valve's steam_api.json (SDK 1.65) and emits one class per interface into src/generated/interfaces/: 25 classes, 801 functions. Everything is reachable through the flat namespace export.

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

flat also carries the generated enums, consts, struct TypeScript interfaces, structLayouts, layoutOf(), callbacksById, and callbackId.

For the hand-written convenience layer over workshop items, see Workshop. For Steam itself, see Core API.

Reaching an interface

Six interfaces have cached getters on Steam:

const steam = init({ appId: 480 });

steam.user;      // ISteamUser
steam.friends;   // ISteamFriends
steam.utils;     // ISteamUtils
steam.apps;      // ISteamApps
steam.userStats; // ISteamUserStats
steam.ugc;       // ISteamUGC

Every other class is constructed directly from steam.native:

const http = new flat.ISteamHTTP(steam.native);
const remote = new flat.ISteamRemoteStorage(steam.native);

The constructor calls the versioned accessor for that interface (for example SteamAPI_SteamUGC_v021) and stores the returned pointer on this.ptr. If the accessor returns null it throws steamwand: SteamAPI_SteamUGC_v021 returned null (is Steam initialized?). So construct only after init() has returned.

Construction is cheap but not free: each new calls the accessor again. Hold on to one instance per interface instead of building one per call. The six getters already do this through an internal cache.

SteamNative.func() caches every koffi function object by symbol name, so two instances of the same class share the registered functions.

The generated JSDoc

Every generated method carries the information you need to call it, so hover it in your editor before reaching for Valve's site. Each one gives you the original C signature, the flat symbol name, a @param line for each pointer parameter saying what to allocate (Buffer you allocate for uint32 *: Buffer.alloc(4) per element), a @remarks line when the call is asynchronous or raises a callback, and an @see link to the matching page on partner.steamgames.com.

Type mapping

The generator maps each C parameter and return type as follows.

C type in steam_api.json TypeScript What you pass or get
const char * (parameter) string A JS string. koffi copies it.
SteamParamStringArray_t * SteamParamStringArrayJs stringArray(['a', 'b'])
HServerListRequest unknown An opaque pointer. Pass it straight back.
any other * or & parameter Buffer | null You allocate the bytes (in or out).
bool boolean
char, int8, uint8, int16, uint16, int32, uint32, int number
float, double number
any enum type number int32. Use flat.EResult.k_EResultOK and friends.
int64, uint64, size_t, intptr_t, CSteamID, CGameID (parameter) bigint | number 1234n or 1234. Above 2^53 you must use bigint.
void return void
const char * return string koffi copies the C string into a JS string.
any other pointer return unknown An opaque handle. Only useful as an argument to another flat function.
64-bit scalar return bigint Always bigint, never number.

Typedefs are followed to their base type, so AppId_t is number, PublishedFileId_t is bigint | number as a parameter and bigint as a return, and UGCQueryHandle_t is the same.

Note the asymmetry: 64-bit values are accepted as bigint | number but always returned as bigint. Do not compare a returned handle to a number literal.

const state = steam.ugc.GetItemState(3786319531n); // number, a flag field
const owner: bigint = steam.apps.GetAppOwner();    // CSteamID, always bigint

if (owner !== steam.steamId()) throw new Error('not the owner');

Out parameters

Every pointer parameter that is not const char * arrives as Buffer | null. The flat API never allocates for you, so you allocate a Buffer of the right size, pass it, and read it back. Steam does not tell you the size at runtime, so it comes from the function's documented buffer-size argument or from a struct layout.

String out buffer

Pass the buffer and its size, then cut at the first NUL byte.

const buf = Buffer.alloc(1024);
const len = steam.apps.GetAppInstallDir(1158310, buf, buf.length);
const dir = buf.toString('utf8', 0, Math.max(buf.indexOf(0), 0));

buf.indexOf(0) returns -1 when Steam filled the buffer completely without a terminator, so the Math.max guard is not decoration. The return value of GetAppInstallDir is the length Steam wrote, but many other functions return boolean and give you no length at all, so read to the NUL either way.

64-bit out parameters

Allocate 8 bytes per value and read with readBigUInt64LE (or readBigInt64LE for a signed field).

const processed = Buffer.alloc(8);
const total = Buffer.alloc(8);

const status = steam.ugc.GetItemUpdateProgress(updateHandle, processed, total);
console.log(status, processed.readBigUInt64LE(0), total.readBigUInt64LE(0));

32-bit out parameters

Four bytes each, readUInt32LE or readInt32LE. A bool * out parameter is one byte: read it with readUInt8(0) !== 0.

const width = Buffer.alloc(4);
const height = Buffer.alloc(4);

if (steam.utils.GetImageSize(imageIndex, width, height)) {
  console.log(width.readUInt32LE(0), height.readUInt32LE(0));
}

Struct out parameter

Size the buffer from the generated layout, then decode it with decodeStruct. layoutOf picks the offset table for the current platform.

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

const layout = flat.layoutOf('SteamUGCDetails_t');
const buf = Buffer.alloc(layout.size);

if (steam.ugc.GetQueryUGCResult(queryHandle, 0, buf)) {
  const details = decodeStruct<flat.SteamUGCDetails_t>(buf, layout);
  console.log(details.m_rgchTitle, details.m_ulSteamIDOwner);
}

layoutOf throws steamwand: no generated layout for struct X for a name it does not know, including the union structs listed under Limits. decodeStruct throws if the buffer is smaller than layout.size.

Fixed char[N] fields decode to string, nested structs and non-char arrays decode to Buffer, and every 64-bit field decodes to bigint.

Array out parameter

A counted array is one buffer of count * elementSize bytes, read with a stride.

const count = steam.ugc.GetNumSubscribedItems(false);
const buf = Buffer.alloc(count * 8); // PublishedFileId_t is 8 bytes
const written = steam.ugc.GetSubscribedItems(buf, count, false);

const ids: bigint[] = [];
for (let i = 0; i < written; i++) ids.push(buf.readBigUInt64LE(i * 8));

The same buffer works as an in parameter. CreateQueryUGCDetailsRequest takes an array of file ids you fill yourself:

const ids = [3786319531n, 2504470131n];
const inBuf = Buffer.alloc(ids.length * 8);
ids.forEach((id, i) => inBuf.writeBigUInt64LE(id, i * 8));

const handle = steam.ugc.CreateQueryUGCDetailsRequest(inBuf, ids.length);

String arrays

SteamParamStringArray_t * is the one struct parameter with a helper. Pass the object that stringArray() returns and koffi marshals the char ** for you.

import { stringArray } from 'steamwand.js';

steam.ugc.AddRequiredTagGroup(queryHandle, stringArray(['gameplay', 'ui']));

Call results

A method whose JSDoc says "Returns an API call handle" is asynchronous. It gives you a bigint handle, not a value, and the remark names the struct to await:

@remarks Returns an API call handle. Await it with
`steam.dispatch.callResultStruct<SteamUGCQueryCompleted_t>(handle, layoutOf('SteamUGCQueryCompleted_t'))`.

In your own code that is one await, with layoutOf reached through flat:

const call = steam.userStats.RequestUserStats(steam.steamId());

const r = await steam.dispatch.callResultStruct<flat.UserStatsReceived_t>(
  call,
  flat.layoutOf('UserStatsReceived_t'),
);

if (r.m_eResult !== flat.EResult.k_EResultOK) {
  throw new Error(`RequestUserStats: ${r.m_eResult}`);
}

76 of the 801 functions are call results. The struct name in the remark is also the layout name, so layoutOf(name) and the flat.<name> TypeScript interface always line up.

callResult(handle) gives you the raw bytes instead, which is what you want when the struct has no generated layout:

const bytes: Buffer = await steam.dispatch.callResult(call);

Both reject with SteamApiCallError when the handle is 0n (Steam refused the call), when SteamAPI_ManualDispatch_GetAPICallResult fails, or when Steam reports an IO failure for that call. They reject with a plain Error if steam.close() runs while the call is still in flight.

k_uAPICallInvalid is 0n, so if (call === flat.k_uAPICallInvalid) is a valid early check before awaiting.

Plain callbacks

Callbacks are not tied to one call. A method that raises one says so in its JSDoc ("Fires the SteamInputConfigurationLoaded_t callback"), but most callbacks arrive on their own. Subscribe by struct name and steamwand looks the id up in callbacksById and decodes with the layout for this platform.

const off = steam.on<flat.ItemInstalled_t>('ItemInstalled_t', (data) => {
  console.log(data.m_unAppID, data.m_nPublishedFileId);
});

off(); // unsubscribe

An unknown name throws steamwand: unknown callback struct 'X' at subscribe time. 191 callback structs have ids and layouts.

For raw bytes, or for an id with no generated layout, subscribe on the dispatch object directly:

const off = steam.dispatch.on(flat.callbackId.ItemInstalled_t, (bytes) => {
  console.log(bytes.length);
});

flat.callbackId maps struct name to id; flat.callbacksById maps id back to { id, name, win64, posix }.

Enums and consts

Enums are as const objects with Valve's own member names, so they are values, not TypeScript enum types. Enum parameters are plain number.

steam.friends.GetFriendCount(flat.EFriendFlags.k_EFriendFlagImmediate);

const h = http.CreateHTTPRequest(flat.EHTTPMethod.k_EHTTPMethodGET, url);

116 enums and 97 consts are generated. Consts keep their C names and their C type: integer consts are number, 64-bit consts are bigint.

flat.k_cchPublishedDocumentTitleMax; // 129
flat.k_uAPICallInvalid;              // 0n
flat.k_PublishedFileIdInvalid;       // 0n

Generated interfaces

25 interfaces, in the order they are exported from src/generated/index.ts. "Getter" is the cached property on Steam; the rest need new flat.X(steam.native).

Class Accessor Getter Functions
ISteamUser SteamAPI_SteamUser_v023 steam.user 33
ISteamFriends SteamAPI_SteamFriends_v018 steam.friends 78
ISteamUtils SteamAPI_SteamUtils_v011 steam.utils 38
ISteamMatchmaking SteamAPI_SteamMatchmaking_v009 38
ISteamMatchmakingServers SteamAPI_SteamMatchmakingServers_v003 18
ISteamParties SteamAPI_SteamParties_v002 11
ISteamRemoteStorage SteamAPI_SteamRemoteStorage_v016 59
ISteamUserStats SteamAPI_SteamUserStats_v013 steam.userStats 44
ISteamApps SteamAPI_SteamApps_v009 steam.apps 35
ISteamNetworking SteamAPI_SteamNetworking_v006 20
ISteamScreenshots SteamAPI_SteamScreenshots_v003 9
ISteamMusic SteamAPI_SteamMusic_v001 9
ISteamHTTP SteamAPI_SteamHTTP_v003 25
ISteamInput SteamAPI_SteamInput_v007 44
ISteamController SteamAPI_SteamController_v008 31
ISteamUGC SteamAPI_SteamUGC_v021 steam.ugc 99
ISteamHTMLSurface SteamAPI_SteamHTMLSurface_v005 37
ISteamInventory SteamAPI_SteamInventory_v003 38
ISteamTimeline SteamAPI_SteamTimeline_v004 18
ISteamVideo SteamAPI_SteamVideo_v007 4
ISteamParentalSettings SteamAPI_SteamParentalSettings_v001 6
ISteamRemotePlay SteamAPI_SteamRemotePlay_v004 20
ISteamNetworkingMessages SteamAPI_SteamNetworkingMessages_SteamAPI_v002 6
ISteamNetworkingSockets SteamAPI_SteamNetworkingSockets_SteamAPI_v013 47
ISteamNetworkingUtils SteamAPI_SteamNetworkingUtils_SteamAPI_v004 34

The accessor version is baked into the generated class, so an SDK bump is a regeneration and the diff shows you which versions moved. See Regenerating.

ISteamNetworkingUtils is the only class built from a global accessor; the other 24 use the user accessor. Interfaces with no accessor in steam_api.json are not generated at all: ISteamClient, ISteamGameServer, ISteamGameServerStats, ISteamNetworkingFakeUDPPort, and the five ISteamMatchmaking*Response callback interfaces.

Limits

18 functions are skipped. The generator drops a function when a parameter or return type has no FFI mapping, and prints every skip when it runs. The current set:

  • C function pointer parameters (11): ISteamUtils::SetWarningMessageHook, ISteamInput::EnableActionEventCallbacks, ISteamNetworkingUtils::SetDebugOutputFunction, and the six ISteamNetworkingUtils::SetGlobalCallback_* setters.
  • Structs passed or returned by value (7): GetDigitalActionData, GetAnalogActionData, and GetMotionData on both ISteamInput and ISteamController; ISteamParties::GetBeaconLocationData; and ISteamNetworking::CreateListenSocket / CreateConnectionSocket, which take a SteamIPAddress_t by value.

There is no workaround inside steamwand for these. For the networking global callbacks, subscribe to the matching callback struct with steam.on() instead of installing a C function pointer. For the Steam Input action data, no alternative exists in the flat API.

Ten structs have no layout. steam_api.json cannot express a C union, and a layout computed from its field list would decode garbage, so the generator excludes those structs and everything that embeds them: SteamNetworkingIdentity, SteamNetworkingIPAddr, SteamNetworkingMessage_t, SteamInputActionEvent_t, SteamNetConnectionInfo_t, SteamDatagramGameCoordinatorServerLogin, SteamNetworkingMessagesSessionRequest_t, SteamNetworkingMessagesSessionFailed_t, SteamNetConnectionStatusChangedCallback_t, and SteamNetworkingFakeIPResult_t.

layoutOf() on any of these throws, and steam.on() cannot decode the four that are callbacks. Functions that take one as a pointer still work: you get Buffer | null and you own the bytes. Read them with readUInt32LE and friends against the field offsets in Valve's steamnetworkingtypes.h, or subscribe with steam.dispatch.on(id, cb) and parse the raw bytes yourself.

An FFI mistake aborts the process. A wrong buffer size or a null pointer where Steam expected memory is a segfault inside the Steam DLL, not a JavaScript exception. Nothing in try/catch helps. See How It Works for the child-process recommendation, and Troubleshooting.

Game server APIs are not wired up. ISteamGameServer and ISteamGameServerStats carry no accessor in steam_api.json, so no class is generated for them, and SteamNative loads SteamAPI_InitFlat only, not the game server init entry point. Their callback structs (GSClientApprove_t and the rest) are still generated, but nothing raises them.

Clone this wiki locally