Steam input and platform services
Use Steam's input, social, media, and utility APIs.
Start with Steam setup. These APIs use the signed-in Steam account. For shared progress across stores, use Crossplay progression.
Stats and achievements
Publish your stat and achievement API names in Steamworks before testing. Stat types must match their definitions. After adding a definition, restart Steam if the client still rejects its name.
Query before displaying cached progress. Write the new total for a Steam stat; writing 1 does not add one. Indicate SIK Achievement Progress shows a notification; it does not save progress or unlock the achievement. Valve’s achievement API
Read the Wins stat
Loading the interactive viewer.
Save the Wins total
Loading the interactive viewer.
Read achievement definitions and states
Loading the interactive viewer.
Unlock the first-win achievement
Loading the interactive viewer.
Show an achievement progress notification
Loading the interactive viewer.
Game module dependencies: SIKCore, SteamIntegrationShared, BetideCore, Engine.
Resolve interfaces through FSIKOnlineServicesFactory::Get(WorldContextObject). Check the services and each interface before use. Bind completion delegates with CreateWeakLambda when they access a UObject.
#include "Interfaces/SIKOnlineServicesFactory.h"
#include "Interfaces/ISIKStats.h"
#include "Interfaces/ISIKAchievements.h"
bool SaveWins(UObject *WorldContextObject, int32 TotalWins, const FSIKAsyncCallbackVoid &Completion)
{
const auto Services = FSIKOnlineServicesFactory::Get(WorldContextObject);
const auto Stats = Services ? Services->GetStatsInterface() : nullptr;
if (!Stats || TotalWins < 0)
return false;
FSIKStatUpdate Update;
Update.Name = TEXT("Wins");
Update.Value = TotalWins;
Stats->UpdateStats(0, {Update}, Completion);
return true;
}
void UnlockFirstWin(ISIKAchievements &Achievements, const FSIKAsyncCallbackVoid &Completion)
{
Achievements.UnlockAchievements(0, {TEXT("FIRST_WIN")}, Completion);
}
void ReadWins(ISIKStats &Stats, const FSIKAccountId &Player,
const FSIKAsyncCallback<FSIKQueryStatsResult> &Completion)
{
Stats.QueryStats(0, Player, {TEXT("Wins")}, Completion);
}
bool ReadSteamAchievements(
UObject *Listener, TSharedPtr<ISIKAchievements> Achievements,
FSIKAsyncCallback<FSIKQueryAchievementDefinitionsResult> DefinitionsReceived,
FSIKAsyncCallback<FSIKQueryAchievementStatesResult> StatesReceived)
{
if (!IsValid(Listener) || !Achievements)
return false;
Achievements->QueryAchievementDefinitions(
0, FSIKAsyncCallback<FSIKQueryAchievementDefinitionsResult>::CreateWeakLambda(
Listener, [Achievements, DefinitionsReceived, StatesReceived](
const TSIKAsyncResult<FSIKQueryAchievementDefinitionsResult> &Result) {
DefinitionsReceived.ExecuteIfBound(Result);
if (Result.IsSuccess())
Achievements->QueryAchievementStates(0, StatesReceived);
else
StatesReceived.ExecuteIfBound(
TSIKAsyncResult<FSIKQueryAchievementStatesResult>::Failure(
Result.GetError()));
}));
return true;
}
void ShowSteamAchievementProgress(ISIKAchievements &Achievements, const FString &Id, int32 Current,
int32 Maximum, const FSIKAsyncCallbackVoid &Completion)
{
Achievements.IndicateProgress(0, Id, Current, Maximum, Completion);
}Read integer values from Result.GetResult().UserStats.Stats; use Find(TEXT("Wins")) to distinguish a missing value from zero. Definitions return Definitions; states return States, matched by AchievementId. Complete the definitions query before reading states. State Progress is 0 to 1. For float stats, set bUseFloatValue and FloatValue on the update.
Leaderboards
Use a stable leaderboard ID. Choose descending for high scores or ascending for times. Write SIK Leaderboard keeps the better score; C++ also supports ForceUpdate.
Find a board and submit a score
Loading the interactive viewer.
Read top scores
Loading the interactive viewer.
Read scores near the local player
Loading the interactive viewer.
Read selected players scores
Loading the interactive viewer.
Obtain GetLeaderboardsInterface() from the services object.
#include "Interfaces/ISIKLeaderboards.h"
bool SubmitScore(UObject *Listener, TSharedPtr<ISIKLeaderboards> Leaderboards, int64 Score,
FSIKAsyncCallbackVoid Completion)
{
if (!IsValid(Listener) || !Leaderboards)
return false;
Leaderboards->FindOrCreateLeaderboard(
0, TEXT("HighScore"), ESIKLeaderboardSortMethod::Descending,
ESIKLeaderboardDisplayType::Numeric,
FSIKAsyncCallback<FSIKLeaderboardDefinition>::CreateWeakLambda(
Listener, [Leaderboards, Score,
Completion](const TSIKAsyncResult<FSIKLeaderboardDefinition> &Result) {
if (!Result.IsSuccess())
{
Completion.ExecuteIfBound(TSIKAsyncResult<void>::Failure(Result.GetError()));
return;
}
Leaderboards->WriteLeaderboard(0, TEXT("HighScore"), Score, Completion,
ESIKLeaderboardUpdateMethod::KeepBest);
}));
return true;
}
void ReadTopTen(ISIKLeaderboards &Leaderboards,
const FSIKAsyncCallback<FSIKLeaderboardResult> &Completion)
{
Leaderboards.ReadEntriesAroundRank(0, TEXT("HighScore"), 1, 10, Completion);
}
void ReadNearbyScores(ISIKLeaderboards &Leaderboards, const FSIKAccountId &Player,
const FSIKAsyncCallback<FSIKLeaderboardResult> &Completion)
{
Leaderboards.ReadEntriesAroundUser(0, TEXT("HighScore"), Player, 5, 10, Completion);
}
void ReadPlayerScores(ISIKLeaderboards &Leaderboards, const TArray<FSIKAccountId> &Players,
const FSIKAsyncCallback<FSIKLeaderboardResult> &Completion)
{
Leaderboards.ReadEntriesForUsers(0, TEXT("HighScore"), Players, Completion);
}Use the local account from GetAuthInterface()->GetAccountId(0) for nearby scores, or account IDs from your friend list for selected-player scores. The write example finds or creates the board before submitting. Results contain entries, ranks, and scores; GetCachedLeaderboard only reads an earlier result. The current nearby query ignores Offset; OSSv2 centers it on the signed-in Steam player. Use the selected-player query for other accounts.
Cloud saves
Enable Steam Cloud and configure its quotas for your app. Save serialized bytes under a stable filename. Keep a local save when cloud storage is unavailable; Steam-only files do not automatically reach other stores.
Write a cloud file
Loading the interactive viewer.
Read a cloud file
Loading the interactive viewer.
List cloud files
Loading the interactive viewer.
Delete a cloud file
Loading the interactive viewer.
Obtain GetUserFileInterface() from the services object.
#include "Interfaces/ISIKUserFile.h"
void SaveProgress(ISIKUserFile &Files, const TArray<uint8> &Bytes,
const FSIKAsyncCallbackVoid &Completion)
{
Files.WriteFile(0, TEXT("progress.sav"), Bytes, Completion);
}
void LoadProgress(ISIKUserFile &Files, const FSIKAsyncCallback<FSIKReadFileResult> &Completion)
{
Files.ReadFile(0, TEXT("progress.sav"), Completion);
}
void ListCloudFiles(ISIKUserFile &Files,
const FSIKAsyncCallback<FSIKEnumerateFilesResult> &Completion)
{
Files.EnumerateFiles(0, Completion);
}
void DeleteCloudFile(ISIKUserFile &Files, const FString &Filename,
const FSIKAsyncCallbackVoid &Completion)
{
Files.DeleteFile(0, Filename, Completion);
}Read FileContents from the successful result and pass it to your save decoder. Enumeration returns Files with each filename and size. GetEnumeratedFiles needs a completed EnumerateFiles request; GetQuota reports available space.
Friends, presence, and overlay
Use the Steam friend list for Steam-only UI, or Crossplay friends for a combined list. Clear presence when leaving the activity it describes. Lobby and session invitations are covered in Steam multiplayer.
List Steam friends
Loading the interactive viewer.
Set and clear presence
Loading the interactive viewer.
Open the friends overlay
Loading the interactive viewer.
The SDK examples below use the SteamIntegrationKit module.
#include "Nodes/Friends/SIK_FriendsLibrary.h"
#include "Nodes/Utils/SIK_UtilsLibrary.h"
bool ReadSteamFriendNames(TArray<FString> &Names)
{
Names.Reset();
const TArray<TEnumAsByte<ESIK_FriendFlags>> Flags = {FriendFlagImmediate};
const int32 Count = USIK_FriendsLibrary::GetFriendCount(Flags);
if (Count < 0)
return false;
Names.Reserve(Count);
for (int32 Index = 0; Index < Count; ++Index)
{
const auto Friend = USIK_FriendsLibrary::GetFriendByIndex(Index, Flags);
Names.Add(USIK_FriendsLibrary::GetFriendPersonaName(Friend));
}
return true;
}
bool SetMatchStatus(const FString &Status)
{
return USIK_FriendsLibrary::SetRichPresence(TEXT("status"), Status);
}
void ClearMatchStatus()
{
USIK_FriendsLibrary::ClearRichPresence();
}
bool ShowSteamFriends()
{
if (!USIK_UtilsLibrary::IsOverlayEnabled())
return false;
USIK_FriendsLibrary::ActivateGameOverlay(TEXT("Friends"));
return true;
}GetFriendCount and GetFriendByIndex take the same friend flags. Avatar and persona data may arrive later; refresh UI from the Friends subsystem callbacks.
Steam Input and controller prompts
Define and publish your actions and controller bindings in Steamworks. Names below must match that configuration. Steam Input setup
Initialize once, cache action handles, and activate the right action set for each connected controller. With Init(false), the Steam callback pump updates input; Init(true) requires your own Run Frame calls. Shut down when the owning input integration stops. Create GameplaySet (SIK Input Action Set Handle), JumpAction (SIK Input Digital Action Handle), and InputReady (Boolean, false). Call the read event each input update after setup; pass its controller and cached handles to the prompt example. Refresh prompts after rebinding.
Initialize, read, and stop Steam Input
Loading the interactive viewer.
Read prompts and open controller bindings
Loading the interactive viewer.
#include "Nodes/Input/SIK_InputLibrary.h"
bool StartInput(FSIK_InputActionSetHandle &Gameplay, FSIK_InputDigitalActionHandle &Jump)
{
if (!USIK_InputLibrary::Init(false))
return false;
Gameplay = USIK_InputLibrary::GetActionSetHandle(TEXT("Gameplay"));
Jump = USIK_InputLibrary::GetDigitalActionHandle(TEXT("Jump"));
if (Gameplay.Result != 0 && Jump.Result != 0)
return true;
USIK_InputLibrary::Shutdown();
return false;
}
bool IsJumpHeld(FSIK_InputHandle Controller, FSIK_InputDigitalActionHandle Jump)
{
const auto Data = USIK_InputLibrary::GetDigitalActionData(Controller, Jump);
return Data.bActive && Data.bState;
}
TArray<bool> ReadJumpForControllers(FSIK_InputActionSetHandle Gameplay,
FSIK_InputDigitalActionHandle Jump)
{
TArray<FSIK_InputHandle> Controllers;
const int64 Count = USIK_InputLibrary::GetConnectedControllers(Controllers);
TArray<bool> Held;
Held.Reserve(static_cast<int32>(Count));
for (int32 Index = 0; Index < Count && Index < Controllers.Num(); ++Index)
{
USIK_InputLibrary::ActivateActionSet(Controllers[Index], Gameplay);
Held.Add(IsJumpHeld(Controllers[Index], Jump));
}
return Held;
}
TArray<FString> ReadJumpPrompts(FSIK_InputHandle Controller, FSIK_InputActionSetHandle Gameplay,
FSIK_InputDigitalActionHandle Jump)
{
TArray<TEnumAsByte<ESIK_InputActionOrigin>> Origins;
USIK_InputLibrary::GetDigitalActionOrigins(Controller, Gameplay, Jump, Origins);
TArray<FString> Paths;
Paths.Reserve(Origins.Num());
for (const auto Origin : Origins)
Paths.Add(USIK_InputLibrary::GetGlyphForActionOrigin(Origin));
return Paths;
}
bool OpenControllerBindings(FSIK_InputHandle Controller)
{
return USIK_InputLibrary::ShowBindingPanel(Controller);
}
bool StopSteamInput()
{
return USIK_InputLibrary::Shutdown();
}Call ReadJumpForControllers each input update after successful setup. It uses the returned controller count because the array can contain unused slots. IsJumpHeld returns held state; detect its rising edge for a single press. Prompt paths are local image files, not Unreal textures.
Screenshots and recording timeline
Trigger Screenshot uses Steam’s capture. If you enable Hook Screenshots, handle the Screenshots subsystem’s request event and supply the image yourself. Timeline events mark moments in the player’s recording; they do not export a video.
Loading the interactive viewer.
#include "Nodes/Screenshots/SIK_ScreenshotsLibrary.h"
#include "Nodes/Timeline/SIK_TimelineLibrary.h"
void CaptureVictory()
{
USIK_ScreenshotsLibrary::TriggerScreenshot();
USIK_TimelineLibrary::SetTimelineGameMode(ESIK_TimelineGameMode_Playing);
USIK_TimelineLibrary::AddTimelineEvent(TEXT("steam_achievement"), TEXT("Victory"),
TEXT("Match won"), 100, 0.0f, 0.0f,
ESIK_TimelineEventClipPriority_Featured);
}Steam party beacons
Beacons advertise available party slots. They do not create a game server or a lobby. Publish a valid connection string, handle OnReservationNotification, and call On Reservation Completed when that player arrives. Update open slots and destroy the beacon when the party closes. These examples manage one hosted beacon at a time. Create AvailableBeacons (SIK Party Beacon ID array) and HostedBeacon (SIK Party Beacon ID). Pass a selected discovered beacon to the join example; its connection string uses the format chosen by your game. Pass only non-negative open-slot counts.
Create AvailableLocations (SIK Steam Party Beacon Location array). Refresh it before offering a hosting location, then pass the selected entry to the host example. Read details for a selected beacon before joining; Steam may remove a beacon between discovery and selection.
Discover hosting locations
Loading the interactive viewer.
Read a selected beacon
Loading the interactive viewer.
List available party beacons
Loading the interactive viewer.
Join a selected party beacon
Loading the interactive viewer.
Host a beacon and confirm reservations
Loading the interactive viewer.
Update party open slots
Loading the interactive viewer.
Join handlers take (TEnumAsByte<ESIK_Result>, FSIK_PartyBeaconID, FSIK_SteamId, FString); create handlers take (TEnumAsByte<ESIK_Result>, FSIK_PartyBeaconID); slot-update handlers take (TEnumAsByte<ESIK_Result>). Reservation handlers take (FSIK_PartyBeaconID, FSIK_SteamId).
Get the Parties subsystem from your GameInstance. Store the created beacon only after ResultOK; if creation fails, remove your reservation delegate. Confirm a reservation from your game’s player-arrival handler.
#include "SteamIntegrationKit.h"
#include "Nodes/Parties/SIK_JoinParty_AsyncFunction.h"
#include "Nodes/Parties/SIK_CreateBeacon_AsyncFunction.h"
#include "Nodes/Parties/SIK_ChangeNumOpenSlots_AsyncFunction.h"
#include "Nodes/Parties/SIK_PartiesLibrary.h"
#include "Nodes/Parties/SIK_PartiesSubsystem.h"
void RefreshBeaconLocations(TArray<FSIK_SteamPartyBeaconLocation> &AvailableLocations)
{
AvailableLocations.Reset();
int32 Count = 0;
if (!USIK_PartiesLibrary::GetNumAvailableBeaconLocations(Count) ||
!USIK_PartiesLibrary::GetAvailableBeaconLocations(Count, AvailableLocations))
{
UE_LOG(LogSIK, Warning, TEXT("Beacon locations unavailable; refresh to retry"));
return;
}
for (const FSIK_SteamPartyBeaconLocation &Location : AvailableLocations)
{
TEnumAsByte<ESIK_SteamPartyBeaconLocationType> Type;
FString Name;
if (USIK_PartiesLibrary::GetBeaconLocationData(Location, Type, Name))
UE_LOG(LogSIK, Log, TEXT("%s"), *Name);
else
UE_LOG(LogSIK, Warning,
TEXT("Location name unavailable; the location is still selectable"));
}
UE_LOG(LogSIK, Log, TEXT("Found %d locations"), AvailableLocations.Num());
}
void InspectPartyBeacon(FSIK_PartyBeaconID Beacon)
{
FSIK_SteamId Creator;
FSIK_SteamPartyBeaconLocation Location;
FString Metadata;
if (!USIK_PartiesLibrary::GetBeaconDetails(Beacon, Creator, Location, Metadata))
{
UE_LOG(LogSIK, Warning, TEXT("Beacon details unavailable; refresh the beacon list"));
return;
}
UE_LOG(LogSIK, Log, TEXT("%s"), *Metadata);
TEnumAsByte<ESIK_SteamPartyBeaconLocationType> Type;
FString Name;
if (USIK_PartiesLibrary::GetBeaconLocationData(Location, Type, Name))
UE_LOG(LogSIK, Log, TEXT("%s"), *Name);
else
UE_LOG(LogSIK, Warning, TEXT("Location name unavailable"));
}
TArray<FSIK_PartyBeaconID> ListPartyBeacons()
{
int32 Count = 0;
USIK_PartiesLibrary::GetNumActiveBeacons(Count);
TArray<FSIK_PartyBeaconID> Beacons;
Beacons.Reserve(Count);
for (int32 Index = 0; Index < Count; ++Index)
Beacons.Add(USIK_PartiesLibrary::GetBeaconByIndex(Index));
return Beacons;
}
bool AdvertiseParty(USIK_PartiesSubsystem *Parties, const FSIK_SteamPartyBeaconLocation &Location,
const FString &ConnectString, const FScriptDelegate &Reservation,
const FScriptDelegate &Success, const FScriptDelegate &Failure)
{
if (!IsValid(Parties) || ConnectString.IsEmpty())
return false;
auto *Request =
USIK_CreateBeacon_AsyncFunction::CreateBeacon(3, Location, ConnectString, TEXT("Coop"));
if (!Request)
return false;
Parties->OnReservationNotification.AddUnique(Reservation);
Request->OnSuccess.Add(Success);
Request->OnFailure.Add(Failure);
Request->Activate();
return true;
}
void ReservedPlayerArrived(FSIK_PartyBeaconID Beacon, FSIK_SteamId Player)
{
USIK_PartiesLibrary::OnReservationCompleted(Beacon, Player);
}
bool UpdatePartyOpenSlots(FSIK_PartyBeaconID Beacon, int32 OpenSlots,
const FScriptDelegate &Success, const FScriptDelegate &Failure)
{
if (OpenSlots < 0)
return false;
auto *Request = USIK_ChangeNumOpenSlots_AsyncFunction::ChangeNumOpenSlots(Beacon, OpenSlots);
if (!Request)
return false;
Request->OnSuccess.Add(Success);
Request->OnFailure.Add(Failure);
Request->Activate();
return true;
}
bool ClosePartyBeacon(USIK_PartiesSubsystem *Parties, FSIK_PartyBeaconID Beacon,
const FScriptDelegate &Reservation)
{
if (!USIK_PartiesLibrary::DestroyBeacon(Beacon))
return false;
if (IsValid(Parties))
Parties->OnReservationNotification.Remove(Reservation);
return true;
}
bool JoinBeacon(FSIK_PartyBeaconID Beacon, const FScriptDelegate &Success,
const FScriptDelegate &Failure)
{
auto *Request = USIK_JoinParty_AsyncFunction::JoinParty(Beacon);
if (!Request)
return false;
Request->OnSuccess.Add(Success);
Request->OnFailure.Add(Failure);
Request->Activate();
return true;
}Utilities and lower-level networking
Use the configured transport for Unreal replication. The SDK networking library also exposes raw P2P packets for custom protocols; accepting a peer and reading packets does not replicate actors. Validate the sender and payload, and close the peer session when done.
The heartbeat uses channel 7 and a single byte (1). Choose a channel unused by your game, give it one reader, and poll once per update or with a bounded loop. Create ExpectedPeer (SIK Steam Id), set it from your authenticated party, and start watching before sending. Stop the old peer before selecting another. These examples use one custom peer; closing its session affects every raw channel to that peer.
Focus your text field before opening the keyboard and pass its screen rectangle. The keyboard types into that field; it does not return the completed text.
Open the player-name keyboard
Loading the interactive viewer.
Read the active Steam App ID
Loading the interactive viewer.
Accept and close an expected custom peer
Loading the interactive viewer.
Send and validate a custom heartbeat
Loading the interactive viewer.
#include "Nodes/Utils/SIK_UtilsLibrary.h"
#include "Nodes/Networking/SIK_NetworkingLibrary.h"
#include "Nodes/Networking/SIK_NetworkingSubsystem.h"
int32 ActiveSteamAppId()
{
return USIK_UtilsLibrary::GetAppID().AppID;
}
bool WatchCustomPeer(USIK_NetworkingSubsystem *Networking, const FScriptDelegate &RequestHandler)
{
if (!IsValid(Networking))
return false;
Networking->OnP2PSessionRequest.AddUnique(RequestHandler);
return true;
}
bool AcceptExpectedPeer(FSIK_SteamId Requested, FSIK_SteamId Expected)
{
return USIK_SharedFile::IsEqualSteamId(Requested, Expected) &&
USIK_NetworkingLibrary::AcceptP2PSessionWithUser(Requested);
}
bool SendCustomHeartbeat(FSIK_SteamId Peer)
{
return USIK_NetworkingLibrary::SendP2PPacket(Peer, {1}, 7, P2PSendUnreliable);
}
bool PollCustomHeartbeat(FSIK_SteamId Expected)
{
int32 Size = 0;
if (!USIK_NetworkingLibrary::IsP2PPacketAvailable(Size, 7))
return false;
TArray<uint8> Bytes;
FSIK_SteamId Sender;
if (!USIK_NetworkingLibrary::ReadP2PPacket(Bytes, Size, Sender, 7))
return false;
return USIK_SharedFile::IsEqualSteamId(Sender, Expected) && Bytes.Num() == 1 && Bytes[0] == 1;
}
bool StopCustomPeer(USIK_NetworkingSubsystem *Networking, FSIK_SteamId Peer,
const FScriptDelegate &RequestHandler)
{
if (IsValid(Networking))
Networking->OnP2PSessionRequest.Remove(RequestHandler);
return USIK_NetworkingLibrary::CloseP2PSessionWithUser(Peer);
}
bool OpenNameKeyboard(int32 X, int32 Y, int32 Width, int32 Height)
{
return USIK_UtilsLibrary::OpenSteamKeyboard(0, X, Y, Width, Height);
}The request handler takes (FSIK_SteamId) and calls AcceptExpectedPeer with your stored peer. PollCustomHeartbeat returns true only for a valid heartbeat. Sending success means queued; it does not confirm delivery.
Steam Web API and build uploads
Call key-protected Steam Web APIs from your backend. Keep publisher and Web API keys out of shipped clients. The SteamWeb module provides request nodes with an On Response result containing the HTTP status and response body.
Loading the interactive viewer.
Add SteamWeb to the backend module. Bind a handler taking (bool, FSIK_BaseWebApiResponse) and check the response before parsing its JSON.
#include "Nodes/User/SIK_GetPlayerSummaries.h"
bool QuerySteamPlayers(const FString &ServerKey, const FString &SteamIds,
const FScriptDelegate &Completion)
{
auto *Request = USIK_GetPlayerSummaries::GetPlayerSummaries(ServerKey, SteamIds);
if (!Request)
return false;
Request->OnResponse.Add(Completion);
Request->Activate();
return true;
}Build uploads run through Betide > Tools, using separate developer credentials. See Steam setup for depot and branch settings.