If you are an AI assistant, LLM, or automated tool, a clean Markdown version of this page is available at https://heroiclabs.com/docs/satori/client-libraries/unreal/llm.md — optimized for AI and LLM tools.
To use Satori in your Unreal project, you’ll need to copy the Satori Client files you downloaded into the appropriate place:
Open your Unreal project folder (for example, C:\\MyUnrealProject\\) in Explorer or Finder.
If one does not already exist, create a Plugins folder here.
Copy the Satori folder from the root of the downloaded release zip and put it in the Plugins folder.
Optionally: you can put the plugin inside your Unreal Engine plugin folder (for example, C:\Program Files\Epic Games\UE_5.3\Engine\Plugins\) to use the plugin across multiple projects.
Remember to add the modules to the dependencies under your project’s Build.cs file. For example:
At this point, you are done. Restart Unreal. After it compiles everything, open Edit->Plugins, then search for “Satori”. If all went well, you should see the Satori plugin, make sure to enable it (this may prompt an editor restart).
The Satori Client connects to a Satori Server and is the entry point to access Satori features. It is recommended to have one client per server per game.
To create a client pass in your server connection details:
/** Maximum number of retry attempts (0 = no retries). */constexprint32MaxRetries=4;/** Base delay in milliseconds before first retry. Actual delay uses exponential backoff with full jitter. */constexprint32BaseDelayMs=500;/** Whether to automatically refresh expired sessions before making API calls. */constexprboolbAutoRefreshSession=true;/** How many seconds before expiry a session is considered "about to expire" and gets refreshed. */constexprint64AutoRefreshBufferSeconds=300;/** HTTP request timeout in seconds. */constexprfloatTimeout=10.0f;FSatoriRetryConfigRetryConfig=FSatoriRetryConfig{MaxRetries,BaseDelayMs,bAutoRefreshSession,AutoRefreshBufferSeconds,Timeout};
This RetryConfig struct can be optionally passed into requests to override the default values.
To be notified when a session is automatically refreshed, for example to persist the new tokens, set the OnSessionRefreshed callback on the config:
1
2
3
4
RetryConfig.OnSessionRefreshed=[](constFSatoriSession&RefreshedSession){// Store RefreshedSession.Token and RefreshedSession.RefreshToken.
};
If the callback captures a UObject pointer, also set OnSessionRefreshedOwner so the callback is skipped if the object has been garbage-collected.
Authenticate users using the Satori Client via their unique ID.
Satori requests are asynchronous, and their callbacks run after the function that started the request has returned. Callbacks must not capture local variables by reference. The examples in this guide assume that ClientConfig, Session and any other state a callback updates, such as UserId, are members of a long-lived object such as your actor, and that WeakThis is a TWeakObjectPtr to that object created from this before the request:
1
2
3
4
5
6
// In the class declaration, for example ASagiShiActor.h
UPROPERTY()FSatoriClientConfigClientConfig;UPROPERTY()FSatoriSessionSession;
When authenticating, you can optionally pass in any desired Default and/or Custom properties to be updated. If none are provided, the properties remain as they are on the server.
constTWeakObjectPtr<ASagiShiActor>WeakThis(this);constFStringId=FGuid::NewGuid().ToString(EGuidFormats::DigitsWithHyphens);constexprboolbNoSession=false;constTMap<FString,FString>Default={};constTMap<FString,FString>Custom={};Satori::Authenticate(ClientConfig,Id,bNoSession,Default,Custom).Next([WeakThis](constFSatoriSessionResult&SessionResult){if(SessionResult.bIsError){UE_LOG(LogTemp,Error,TEXT("Error authenticating: {%d} %s"),SessionResult.Error.Code,*SessionResult.Error.Message);return;}if(!WeakThis.IsValid()){return;}WeakThis->Session=SessionResult.Value;UE_LOG(LogTemp,Log,TEXT("Session has token: %s"),*WeakThis->Session.Token);UE_LOG(LogTemp,Log,TEXT("Session has refresh token: %s"),*WeakThis->Session.RefreshToken);});
When authenticated, the server responds with a JWT (JSON Web Token) containing useful properties, such as the Auth and Refresh tokens, along with the user’s Id and Username, which gets deserialized into a Session object.
For more details on effective session management, check out this guide.
Sessions expire after five (5) minutes by default. Expiring inactive sessions is good security practice.
Satori provides ways to restore sessions, for example, when players re-launch the game, or refresh tokens to keep the session active while the game is being played.
Use the auth and refresh tokens on the session object to restore or refresh sessions.
Store these tokens using your preferred method:
1
2
Session.TokenSession.RefreshToken
Restore a session without having to re-authenticate:
Satori::ListProperties(ClientConfig,Session).Next([](constFSatoriPropertiesResult&ListPropertiesResult){if(ListPropertiesResult.bIsError){UE_LOG(LogTemp,Error,TEXT("Error listing properties: {%d} %s"),ListPropertiesResult.Error.Code,*ListPropertiesResult.Error.Message);return;}for(TTupleProperty:ListPropertiesResult.Value.Default){UE_LOG(LogTemp,Log,TEXT("Default Property found: %s with value %s"),*Property.Key,*Property.Value);}for(TTupleProperty:ListPropertiesResult.Value.Custom){UE_LOG(LogTemp,Log,TEXT("Custom Property found: %s with value %s"),*Property.Key,*Property.Value);}for(TTupleProperty:ListPropertiesResult.Value.Computed){UE_LOG(LogTemp,Log,TEXT("Computed Property found: %s with value %s"),*Property.Key,*Property.Value);}});
If you want to submit events to Satori before a user has authenticated with the game backend (e.g. Nakama) and has a User ID, you should authenticate with Satori using a temporary ID, such as the device’s unique identifier or a randomly generated one.
Once a user has successfully authenticated, you should then call Satori::Identify to enrich the current session and return a new session that should be used for submitting future events.
You can also specify an array of experiment names and/or category labels you wish to get:
1
2
3
4
constTArray<FString>Names={TEXT("ExperimentOne"),TEXT("ExperimentTwo")};constTArray<FString>Labels={TEXT("LabelOne"),TEXT("LabelTwo")};// Same as previous snippet...
constTArray<FString>Names={};constTArray<FString>Labels={};Satori::GetFlags(ClientConfig,Session,Names,Labels).Next([](constFSatoriFlagListResult&FlagListResult){if(FlagListResult.bIsError){UE_LOG(LogTemp,Error,TEXT("Error getting flags: {%d} %s"),FlagListResult.Error.Code,*FlagListResult.Error.Message);return;}for(FSatoriFlagFlag:FlagListResult.Value.Flags){UE_LOG(LogTemp,Log,TEXT("Flag found: %s with value: %s"),*Flag.Name,*Flag.Value);}});
You can also specify an array of feature flag names and/or by category labels you wish to get:
1
2
3
4
constTArray<FString>Names={TEXT("FlagOne"),TEXT("FlagTwo")};constTArray<FString>Labels={TEXT("LabelOne"),TEXT("LabelTwo")};// Same as previous snippet...
constTArray<FString>Names={};constTArray<FString>Labels={};Satori::GetFlagOverrides(ClientConfig,Session,Names,Labels).Next([](constFSatoriFlagOverrideListResult&FlagOverrideListResult){if(FlagOverrideListResult.bIsError){UE_LOG(LogTemp,Error,TEXT("Error getting flags: {%d} %s"),FlagOverrideListResult.Error.Code,*FlagOverrideListResult.Error.Message);return;}for(FSatoriFlagOverrideFlag:FlagOverrideListResult.Value.Flags){UE_LOG(LogTemp,Log,TEXT("Flag found: %s"),*Flag.FlagName);for(FSatoriFlagOverrideValueOverride:Flag.Overrides){UE_LOG(LogTemp,Log,TEXT("Override found: %s with value %s"),*Override.Name,*Override.Value);}}});
You can also specify an array of feature flag names and/or by category labels you wish to get:
1
2
3
4
constTArray<FString>Names={TEXT("FlagOne"),TEXT("FlagTwo")};constTArray<FString>Labels={TEXT("LabelOne"),TEXT("LabelTwo")};// Same as previous snippet...
FSatoriEventEventTwo=FSatoriEvent{};EventTwo.Name=TEXT("adStarted");EventTwo.Timestamp=FDateTime::UtcNow();EventTwo.Metadata={{TEXT("meta"),TEXT("data")}};FSatoriEventEventThree=FSatoriEvent{};EventThree.Name=TEXT("appLaunched");EventThree.Timestamp=FDateTime::UtcNow();EventThree.Metadata={{TEXT("meta"),TEXT("data")}};constTArrayEvents={EventTwo,EventThree};// Same as previous snippet...
// List live events for the current player.
// Returns ACTIVE events by default.
constTArray<FString>Names={};constTArray<FString>Labels={};constexprint32PastRunCount=0;constexprint32FutureRunCount=0;constexprint64StartTimeSec=0;constexprint64EndTimeSec=0;Satori::GetLiveEvents(ClientConfig,Session,Names,Labels,PastRunCount,FutureRunCount,StartTimeSec,EndTimeSec).Next([](constFSatoriLiveEventListResult&LiveEventListResult){if(LiveEventListResult.bIsError){UE_LOG(LogTemp,Error,TEXT("Error listing live events: {%d} %s"),LiveEventListResult.Error.Code,*LiveEventListResult.Error.Message);return;}for(FSatoriLiveEventLiveEvent:LiveEventListResult.Value.LiveEvents){UE_LOG(LogTemp,Log,TEXT("Live Event found: %s with value %s"),*LiveEvent.Name,*LiveEvent.Value);}for(FSatoriLiveEventExplicitJoinLiveEvent:LiveEventListResult.Value.ExplicitJoinLiveEvents){UE_LOG(LogTemp,Log,TEXT("Explicit Join Live Event found: %s with value %s"),*ExplicitJoinLiveEvent.Name,*ExplicitJoinLiveEvent.Value);}});
Include 1 past run and 2 upcoming runs
1
2
3
constexprint32PastRunCount=1;constexprint32FutureRunCount=2;// Same as previous snippet...
Satori integrates with various messaging services to send messages to users via message schedules. You can list a user’s messages, update a message’s status, and delete a message.
constFStringMessageId=TEXT("messageId");// Both ConsumeTime and ReadTime should be Unix Timestamps
constint64ReadTime=FDateTime::UtcNow().ToUnixTimestamp();constint64ConsumeTime=FDateTime::UtcNow().ToUnixTimestamp();Satori::UpdateMessage(ClientConfig,Session,MessageId,ReadTime,ConsumeTime).Next([](constFSatoriVoidResult&UpdateResult){if(UpdateResult.bIsError){UE_LOG(LogTemp,Error,TEXT("Error updating message: {%d} %s"),UpdateResult.Error.Code,*UpdateResult.Error.Message);}});