---
title: "Steam input and platform services"
description: "Use Steam's input, social, media, and utility APIs."
seo:
  noindex: true
---

{/* Copyright (c) 2026 Betide Studio. All Rights Reserved. */}

Start with [Steam setup](/integrations/steam/setup). These APIs use the signed-in Steam account. For shared progress across stores, use [Crossplay progression](/crossplay/achievements-stats).

## 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](https://partner.steamgames.com/doc/api/ISteamUserStats#IndicateAchievementProgress)

**Blueprint**

<Accordion>
  <AccordionItem title="Read the Wins stat">
<BlueprintViewer title="Read the Wins stat" src="/blueprints/crossplay-steam-read-stats.txt" />
  </AccordionItem>
  <AccordionItem title="Save the Wins total">
<BlueprintViewer title="Save the Wins total" src="/blueprints/crossplay-steam-write-stats.txt" />
  </AccordionItem>
  <AccordionItem title="Read achievement definitions and states">
<BlueprintViewer title="Read achievement definitions and states" src="/blueprints/crossplay-steam-read-achievements.txt" />
  </AccordionItem>
  <AccordionItem title="Unlock the first-win achievement">
<BlueprintViewer title="Unlock the first-win achievement" src="/blueprints/crossplay-steam-unlock-achievement.txt" />
  </AccordionItem>
  <AccordionItem title="Show an achievement progress notification">
<BlueprintViewer title="Show an achievement progress notification" src="/blueprints/crossplay-steam-achievement-progress.txt" />
  </AccordionItem>
</Accordion>

**C++**

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.

```cpp
#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`.

**Blueprint**

<Accordion>
  <AccordionItem title="Find a board and submit a score">
<BlueprintViewer title="Find a board and submit a score" src="/blueprints/crossplay-steam-write-score.txt" />
  </AccordionItem>
  <AccordionItem title="Read top scores">
<BlueprintViewer title="Read top scores" src="/blueprints/crossplay-steam-top-scores.txt" />
  </AccordionItem>
  <AccordionItem title="Read scores near the local player">
<BlueprintViewer title="Read scores near the local player" src="/blueprints/crossplay-steam-nearby-scores.txt" />
  </AccordionItem>
  <AccordionItem title="Read selected players scores">
<BlueprintViewer title="Read selected players scores" src="/blueprints/crossplay-steam-player-scores.txt" />
  </AccordionItem>
</Accordion>

**C++**

Obtain `GetLeaderboardsInterface()` from the services object.

```cpp
#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.

**Blueprint**

<Accordion>
  <AccordionItem title="Write a cloud file">
<BlueprintViewer title="Write a cloud file" src="/blueprints/crossplay-steam-write-file.txt" />
  </AccordionItem>
  <AccordionItem title="Read a cloud file">
<BlueprintViewer title="Read a cloud file" src="/blueprints/crossplay-steam-read-file.txt" />
  </AccordionItem>
  <AccordionItem title="List cloud files">
<BlueprintViewer title="List cloud files" src="/blueprints/crossplay-steam-list-files.txt" />
  </AccordionItem>
  <AccordionItem title="Delete a cloud file">
<BlueprintViewer title="Delete a cloud file" src="/blueprints/crossplay-steam-delete-file.txt" />
  </AccordionItem>
</Accordion>

**C++**

Obtain `GetUserFileInterface()` from the services object.

```cpp
#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](/crossplay/friends-presence) for a combined list. Clear presence when leaving the activity it describes. Lobby and session invitations are covered in [Steam multiplayer](/integrations/steam/multiplayer).

**Blueprint**

<Accordion>
  <AccordionItem title="List Steam friends">
<BlueprintViewer title="List Steam friends" src="/blueprints/crossplay-steam-friends.txt" />
  </AccordionItem>
  <AccordionItem title="Set and clear presence">
<BlueprintViewer title="Set and clear presence" src="/blueprints/crossplay-steam-presence.txt" />
  </AccordionItem>
  <AccordionItem title="Open the friends overlay">
<BlueprintViewer title="Open the friends overlay" src="/blueprints/crossplay-steam-overlay.txt" />
  </AccordionItem>
</Accordion>

**C++**

The SDK examples below use the `SteamIntegrationKit` module.

```cpp
#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.

<Accordion>
  <AccordionItem title="Steam Input and controller prompts">

Define and publish your actions and controller bindings in Steamworks. Names below must match that configuration. [Steam Input setup](https://partner.steamgames.com/doc/features/steam_controller/getting_started_for_devs)

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.

**Blueprint**

<Accordion>
  <AccordionItem title="Initialize, read, and stop Steam Input">
<BlueprintViewer title="Initialize, read, and stop Steam Input" src="/blueprints/crossplay-steam-input.txt" />
  </AccordionItem>
  <AccordionItem title="Read prompts and open controller bindings">
<BlueprintViewer title="Read prompts and open controller bindings" src="/blueprints/crossplay-steam-input-prompts.txt" />
  </AccordionItem>
</Accordion>

**C++**

```cpp
#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.

  </AccordionItem>
  <AccordionItem title="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.

**Blueprint**

<BlueprintViewer title="Capture a victory" src="/blueprints/crossplay-steam-victory-capture.txt" />

**C++**

```cpp
#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);
}
```

  </AccordionItem>
  <AccordionItem title="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.

**Blueprint**

<Accordion>
  <AccordionItem title="Discover hosting locations">
<BlueprintViewer title="Discover hosting locations" src="/blueprints/crossplay-steam-beacon-locations.txt" />
  </AccordionItem>
  <AccordionItem title="Read a selected beacon">
<BlueprintViewer title="Read a selected beacon" src="/blueprints/crossplay-steam-beacon-details.txt" />
  </AccordionItem>
  <AccordionItem title="List available party beacons">
<BlueprintViewer title="List available party beacons" src="/blueprints/crossplay-steam-list-beacons.txt" />
  </AccordionItem>
  <AccordionItem title="Join a selected party beacon">
<BlueprintViewer title="Join a selected party beacon" src="/blueprints/crossplay-steam-join-beacon.txt" />
  </AccordionItem>
  <AccordionItem title="Host a beacon and confirm reservations">
<BlueprintViewer title="Host a beacon and confirm reservations" src="/blueprints/crossplay-steam-host-beacon.txt" />
  </AccordionItem>
  <AccordionItem title="Update party open slots">
<BlueprintViewer title="Update party open slots" src="/blueprints/crossplay-steam-beacon-slots.txt" />
  </AccordionItem>
</Accordion>

**C++**

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.

```cpp
#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;
}
```

  </AccordionItem>
  <AccordionItem title="Utilities and lower-level networking">

Use the configured [transport](/start/presets) 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.

**Blueprint**

<Accordion>
  <AccordionItem title="Open the player-name keyboard">
<BlueprintViewer title="Open the player-name keyboard" src="/blueprints/crossplay-steam-keyboard.txt" />
  </AccordionItem>
  <AccordionItem title="Read the active Steam App ID">
<BlueprintViewer title="Read the active Steam App ID" src="/blueprints/crossplay-steam-app-id.txt" />
  </AccordionItem>
  <AccordionItem title="Accept and close an expected custom peer">
<BlueprintViewer title="Accept and close an expected custom peer" src="/blueprints/crossplay-steam-custom-peer.txt" />
  </AccordionItem>
  <AccordionItem title="Send and validate a custom heartbeat">
<BlueprintViewer title="Send and validate a custom heartbeat" src="/blueprints/crossplay-steam-heartbeat.txt" />
  </AccordionItem>
</Accordion>

**C++**

```cpp
#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.

  </AccordionItem>
  <AccordionItem title="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.

**Blueprint**

<BlueprintViewer title="Look up players on the backend" src="/blueprints/crossplay-steam-backend-players.txt" />

**C++**

Add `SteamWeb` to the backend module. Bind a handler taking `(bool, FSIK_BaseWebApiResponse)` and check the response before parsing its JSON.

```cpp
#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](/integrations/steam/setup) for depot and branch settings.

  </AccordionItem>
</Accordion>
