# Unreal

**URL:** https://heroiclabs.com/docs/satori/client-libraries/unreal/
**Summary:** The official Unreal client handles all communication with the Satori server, enabling management of your live game via sending analytics events, updating properties, getting feature flags and experiments, and more.
**Keywords:** unreal client guide, unreal client, satori unreal, authentication, user accounts, audiences, experiments, live events, feature flags, identities, liveops unreal
**Categories:** satori, unreal, client-libraries

---


# Satori Unreal Client Guide

This client library guide will show you how to use the core Satori features in **Unreal**.

## Prerequisites

Before proceeding ensure that you have:

* Access to a Satori server instance
* Installed the [Satori Unreal SDK](#installation)

{{< note "important" >}}
The Satori client is packaged as part of the Nakama Unreal SDK, but using Nakama **is not** required.
{{< / note >}}

### Installation

The client is available from the:

* [Heroic Labs GitHub releases page](https://github.com/heroiclabs/nakama-unreal/releases/latest)

To use Satori in your Unreal project, you'll need to copy the Satori Client files you downloaded into the appropriate place:

1. Open your Unreal project folder (for example, `C:\\MyUnrealProject\\`) in Explorer or Finder.
2. If one does not already exist, create a `Plugins` folder here.
3. 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:

```cs
PrivateDependencyModuleNames.AddRange(new string[] { "Satori" });
```

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).

#### Updates

New versions of the Satori Unreal Client and the corresponding improvements are documented in the [Release Notes](../../../satori/concepts/introduction/release-notes/).

#### Logging

Logging to the console in this guide uses the Unreal specific `UE_LOG` macro. In the Unreal SDK use:

```cpp
UE_LOG(LogTemp, Log, TEXT("Hello, %s!"), TEXT("Satori"));
```

## Getting started

Learn how to get started using the Satori Client to manage your live game.

### Satori client

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:

```cpp
const FString ServerKey = TEXT("apiKey");
const FString Host = TEXT("127.0.0.1");
constexpr int32 Port = 7450;
constexpr bool bUseSSL = false;

ClientConfig = FSatoriClientConfig{ServerKey, Host, Port, bUseSSL};
```

{{< note "important" "API Keys">}}
Create and rotate API keys in the [Satori Settings page](../../concepts/settings/#api-keys).
{{< / note >}}

### Configuring request retries

To configure request retry logic:

```cpp
/** Maximum number of retry attempts (0 = no retries). */
constexpr int32 MaxRetries = 4;
/** Base delay in milliseconds before first retry. Actual delay uses exponential backoff with full jitter. */
constexpr int32 BaseDelayMs = 500;
/** Whether to automatically refresh expired sessions before making API calls. */
constexpr bool bAutoRefreshSession = true;
/** How many seconds before expiry a session is considered "about to expire" and gets refreshed. */
constexpr int64 AutoRefreshBufferSeconds = 300;
/** HTTP request timeout in seconds. */
constexpr float Timeout = 10.0f;

FSatoriRetryConfig RetryConfig = 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:

```cpp
RetryConfig.OnSessionRefreshed = [](const FSatoriSession& 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.

## Authentication

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:

```cpp
// In the class declaration, for example ASagiShiActor.h
UPROPERTY()
FSatoriClientConfig ClientConfig;

UPROPERTY()
FSatoriSession Session;
```

When authenticating, you can optionally pass in any desired [Default and/or Custom properties](../../concepts/segmentation/understand-player-properties/) to be updated. If none are provided, the properties remain as they are on the server.

```cpp
const TWeakObjectPtr<ASagiShiActor> WeakThis(this);

const FString Id = FGuid::NewGuid().ToString(EGuidFormats::DigitsWithHyphens);
constexpr bool bNoSession = false;
const TMap<FString, FString> Default = {};
const TMap<FString, FString> Custom = {};

Satori::Authenticate(ClientConfig, Id, bNoSession, Default, Custom).Next(
    [WeakThis](const FSatoriSessionResult& 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](../../concepts/sessions/session-management/).

### Session lifecycle

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:

```cpp
Session.Token
Session.RefreshToken
```

Restore a session without having to re-authenticate:

```cpp
Session.Update(Token, RefreshToken);
```

#### Automatic session refresh

The Unreal client library includes a feature where sessions close to expiration are automatically refreshed.

This is enabled by default but can be configured when first creating the [Retry Config](#configuring-request-retries) using the following parameters:

* `bAutoRefreshSession` - Boolean value indicating if this feature is enabled, `true` by default
* `AutoRefreshBufferSeconds` - The time prior to session expiry when auto-refresh will occur, set to 5 minutes by default

#### Manually refreshing a session

Sessions can be manually refreshed.

```cpp
Satori::AuthenticateRefresh(ClientConfig, Session.RefreshToken).Next(
    [WeakThis](const FSatoriSessionResult& SessionResult)
    {
       if (SessionResult.bIsError)
       {
          UE_LOG(LogTemp, Error, TEXT("Error refreshing session: {%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);
    });
```

### Ending sessions

Logout and end the current session:

```cpp
Satori::AuthenticateLogout(ClientConfig, Session.Token, Session.RefreshToken).Next(
    [](const FSatoriVoidResult& LogoutResult)
    {
       if (LogoutResult.bIsError)
       {
          UE_LOG(LogTemp, Error, TEXT("Error logging out: {%d} %s"), LogoutResult.Error.Code,
                 *LogoutResult.Error.Message);
       }
    });
```

## Identities

Satori [Identities](../../concepts/segmentation/understand-player-identities/) identify individual players of your game and can be enriched with custom properties.

### List an identity's properties

```cpp
Satori::ListProperties(ClientConfig, Session).Next(
    [](const FSatoriPropertiesResult& ListPropertiesResult)
    {
       if (ListPropertiesResult.bIsError)
       {
          UE_LOG(LogTemp, Error, TEXT("Error listing properties: {%d} %s"),
                 ListPropertiesResult.Error.Code,
                 *ListPropertiesResult.Error.Message);
          return;
       }

       for (TTuple Property : ListPropertiesResult.Value.Default)
       {
          UE_LOG(LogTemp, Log, TEXT("Default Property found: %s with value %s"),
                 *Property.Key, *Property.Value);
       }

       for (TTuple Property : ListPropertiesResult.Value.Custom)
       {
          UE_LOG(LogTemp, Log, TEXT("Custom Property found: %s with value %s"), *Property.Key,
                 *Property.Value);
       }

       for (TTuple Property : ListPropertiesResult.Value.Computed)
       {
          UE_LOG(LogTemp, Log, TEXT("Computed Property found: %s with value %s"),
                 *Property.Key, *Property.Value);
       }
    });
```

### Update an identity's properties

```cpp
constexpr bool bRecompute = true;
const TMap<FString, FString> DefaultProperties = {
    { TEXT("defaultPropertyKey"), TEXT("defaultPropertyValue") },
    { TEXT("anotherDefaultPropertyKey"), TEXT("anotherDefaultPropertyValue") }
};
const TMap<FString, FString> CustomProperties = {
    { TEXT("customPropertyKey"), TEXT("customPropertyValue") },
    { TEXT("anotherCustomPropertyKey"), TEXT("anotherCustomPropertyValue") }
};

Satori::UpdateProperties(ClientConfig, Session, bRecompute, DefaultProperties, CustomProperties).Next(
    [](const FSatoriVoidResult& UpdatePropertiesResult)
    {
       if (UpdatePropertiesResult.bIsError)
       {
          UE_LOG(LogTemp, Error, TEXT("Error updating properties: {%d} %s"),
                 UpdatePropertiesResult.Error.Code,
                 *UpdatePropertiesResult.Error.Message);
       }
    });
```

You can immediately reevaluate the identity's audience memberships upon updating their properties by passing `bRecompute` as `true`:

```cpp
constexpr bool bRecompute = true;

// Same as previous snippet...
```

### Identifying a session with a new ID

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.

```cpp
const FString Id = FPlatformMisc::GetDeviceId();
constexpr bool bNoSession = false;
const TMap<FString, FString> Default = {};
const TMap<FString, FString> Custom = {};

Satori::Authenticate(ClientConfig, Id, bNoSession, Default, Custom).Next(
    [WeakThis](const FSatoriSessionResult& 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);
    });
```

You can then submit events before the user has authenticated with the game backend.

```cpp
FSatoriEvent EventOne = FSatoriEvent{};
EventOne.Name = TEXT("gameLaunched");
EventOne.Timestamp = FDateTime::UtcNow();

const TArray Events = { EventOne };

Satori::Event(ClientConfig, Session, Events).Next([](const FSatoriVoidResult& EventResult)
{
    if (EventResult.bIsError)
    {
       UE_LOG(LogTemp, Error, TEXT("Error publishing event: {%d} %s"), EventResult.Error.Code,
              *EventResult.Error.Message);
    }
});
```

The user would then authenticate with the game backend and retrieve their User ID.

```cpp
Nakama::AuthenticateEmail(NakamaClientConfig, false, TEXT(""), TEXT("example@heroiclabs.com"), TEXT("password")).
    Next([WeakThis](const FNakamaSessionResult& 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->UserId = SessionResult.Value.UserId;
    });
```

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.

```cpp
Satori::Identify(ClientConfig, Session, UserId, Default, Custom).Next(
    [WeakThis](const FSatoriSessionResult& IdentifyResult)
    {
       if (IdentifyResult.bIsError)
       {
          UE_LOG(LogTemp, Error, TEXT("Error enriching session: {%d} %s"),
                 IdentifyResult.Error.Code, *IdentifyResult.Error.Message);
          return;
       }

       if (!WeakThis.IsValid())
       {
          return;
       }

       WeakThis->Session = IdentifyResult.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);
    });
```

Note that the old session is no longer valid and cannot be used after this.

### Deleting an identity

Delete the calling identity and its associated data:

```cpp
Satori::DeleteIdentity(ClientConfig, Session).Next([](const FSatoriVoidResult& DeleteResult)
{
    if (DeleteResult.bIsError)
    {
       UE_LOG(LogTemp, Error, TEXT("Error deleting identity: {%d} %s"), DeleteResult.Error.Code,
              *DeleteResult.Error.Message);
    }
});
```

## Experiments

Satori [Experiments](../../concepts/experiments/) allow you to test different game features and configurations in a live game.

List the current experiments for this identity:

```cpp
const TArray<FString> Names = {};
const TArray<FString> Labels = {};

Satori::GetExperiments(ClientConfig, Session, Names, Labels).Next(
    [](const FSatoriExperimentListResult& ExperimentListResult)
    {
       if (ExperimentListResult.bIsError)
       {
          UE_LOG(LogTemp, Error, TEXT("Error getting experiments out: {%d} %s"),
                 ExperimentListResult.Error.Code,
                 *ExperimentListResult.Error.Message);
          return;
       }

       for (FSatoriExperiment Experiment : ExperimentListResult.Value.Experiments)
       {
          UE_LOG(LogTemp, Log, TEXT("Experiment found: %s"), *Experiment.Name);
       }
    });
```

You can also specify an array of experiment names and/or category labels you wish to get:

```cpp
const TArray<FString> Names = { TEXT("ExperimentOne"), TEXT("ExperimentTwo") };
const TArray<FString> Labels = { TEXT("LabelOne"), TEXT("LabelTwo") };

// Same as previous snippet...
```

## Feature flags

Satori [feature flags](../../concepts/remote-configuration/understand-feature-flags/) allow you to remotely configure game parameters in a live game.

### Listing active feature flags

List all active feature flags for the identity:

```cpp
const TArray<FString> Names = {};
const TArray<FString> Labels = {};

Satori::GetFlags(ClientConfig, Session, Names, Labels).Next(
    [](const FSatoriFlagListResult& FlagListResult)
    {
       if (FlagListResult.bIsError)
       {
          UE_LOG(LogTemp, Error, TEXT("Error getting flags: {%d} %s"),
                 FlagListResult.Error.Code, *FlagListResult.Error.Message);
          return;
       }

       for (FSatoriFlag Flag : 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:

```cpp
const TArray<FString> Names = { TEXT("FlagOne"), TEXT("FlagTwo") };
const TArray<FString> Labels = { TEXT("LabelOne"), TEXT("LabelTwo") };

// Same as previous snippet...
```

### Listing all feature flags

List all available feature flags and their value overrides for this identity:

```cpp
const TArray<FString> Names = {};
const TArray<FString> Labels = {};

Satori::GetFlagOverrides(ClientConfig, Session, Names, Labels).Next(
    [](const FSatoriFlagOverrideListResult& FlagOverrideListResult)
    {
       if (FlagOverrideListResult.bIsError)
       {
          UE_LOG(LogTemp, Error, TEXT("Error getting flags: {%d} %s"),
                 FlagOverrideListResult.Error.Code,
                 *FlagOverrideListResult.Error.Message);
          return;
       }

       for (FSatoriFlagOverride Flag : FlagOverrideListResult.Value.Flags)
       {
          UE_LOG(LogTemp, Log, TEXT("Flag found: %s"), *Flag.FlagName);

          for (FSatoriFlagOverrideValue Override : 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:

```cpp
const TArray<FString> Names = { TEXT("FlagOne"), TEXT("FlagTwo") };
const TArray<FString> Labels = { TEXT("LabelOne"), TEXT("LabelTwo") };

// Same as previous snippet...
```

## Events

Satori [Events](../../concepts/performance-monitoring/understand-events/) allow you to send data for a given identity to the server for processing.

{{< note "important" "Metadata Limits">}}
The maximum size of the `metadata` field is `4096` bytes.
{{< / note >}}

### Sending single events

```cpp
FSatoriEvent EventOne = FSatoriEvent{};
EventOne.Name = TEXT("gameFinished");
EventOne.Timestamp = FDateTime::UtcNow();
EventOne.Metadata = { { TEXT("meta"), TEXT("data") } };

const TArray Events = { EventOne };

Satori::Event(ClientConfig, Session, Events).Next([](const FSatoriVoidResult& EventResult)
{
    if (EventResult.bIsError)
    {
       UE_LOG(LogTemp, Error, TEXT("Error publishing event: {%d} %s"), EventResult.Error.Code,
              *EventResult.Error.Message);
    }
});
```

### Sending multiple events

```cpp
FSatoriEvent EventTwo = FSatoriEvent{};
EventTwo.Name = TEXT("adStarted");
EventTwo.Timestamp = FDateTime::UtcNow();
EventTwo.Metadata = { { TEXT("meta"), TEXT("data") } };

FSatoriEvent EventThree = FSatoriEvent{};
EventThree.Name = TEXT("appLaunched");
EventThree.Timestamp = FDateTime::UtcNow();
EventThree.Metadata = { { TEXT("meta"), TEXT("data") } };

const TArray Events = { EventTwo, EventThree };

// Same as previous snippet...
```

### Sending events from your server

To send events from your game server using the Satori HTTP key instead of a player session, use `Satori::ServerEvent`:

```cpp
const FString HttpKey = TEXT("<HttpKey>");

Satori::ServerEvent(ClientConfig, HttpKey, Events).Next([](const FSatoriVoidResult& EventResult)
{
    if (EventResult.bIsError)
    {
       UE_LOG(LogTemp, Error, TEXT("Error publishing event: {%d} %s"), EventResult.Error.Code,
              *EventResult.Error.Message);
    }
});
```

## Live events

Satori [Live Events](../../concepts/live-events/) allow you to deliver established features to your players on a custom schedule.

### List live events

Get active, past, and upcoming live events for the authenticated player.

You can optionally include a number of past or future runs, or filter by event name.
- **Names** — optional list of event names to filter
- **Labels** — optional list of category labels to filter
- **PastRunCount** — number of past runs to return
- **FutureRunCount** — number of upcoming runs to return

```cpp
// List live events for the current player.
// Returns ACTIVE events by default.

const TArray<FString> Names = {};
const TArray<FString> Labels = {};
constexpr int32 PastRunCount = 0;
constexpr int32 FutureRunCount = 0;
constexpr int64 StartTimeSec = 0;
constexpr int64 EndTimeSec = 0;

Satori::GetLiveEvents(ClientConfig, Session, Names, Labels, PastRunCount, FutureRunCount, StartTimeSec, EndTimeSec).
    Next([](const FSatoriLiveEventListResult& LiveEventListResult)
    {
       if (LiveEventListResult.bIsError)
       {
          UE_LOG(LogTemp, Error, TEXT("Error listing live events: {%d} %s"),
                 LiveEventListResult.Error.Code,
                 *LiveEventListResult.Error.Message);
          return;
       }

       for (FSatoriLiveEvent LiveEvent : LiveEventListResult.Value.LiveEvents)
       {
          UE_LOG(LogTemp, Log, TEXT("Live Event found: %s with value %s"), *LiveEvent.Name,
                 *LiveEvent.Value);
       }

       for (FSatoriLiveEvent ExplicitJoinLiveEvent : 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

```cpp
constexpr int32 PastRunCount = 1;
constexpr int32 FutureRunCount = 2;
// Same as previous snippet...
```

Filter by specific event names

```cpp
const TArray<FString> Names = { TEXT("Season Pass") };
```

Each returned event includes a `status` field:
- **TERMINATED** — past events
- **ACTIVE** — ongoing events
- **UPCOMING** — scheduled but not yet started

### Join live event explicitly

```cpp
const FString LiveEventId = TEXT("live-event-id");

Satori::JoinLiveEvent(ClientConfig, Session, LiveEventId).Next([](const FSatoriVoidResult& JoinResult)
{
    if (JoinResult.bIsError)
    {
       UE_LOG(LogTemp, Error, TEXT("Error joining live event: {%d} %s"), JoinResult.Error.Code,
              *JoinResult.Error.Message);
    }
});
```

## Messages

Satori [integrates](../../concepts/player-messaging/message-integrations/) with various messaging services to send messages to users via [message schedules](../../concepts/player-messaging/automate-a-message). You can list a user's messages, update a message's status, and delete a message.

### List messages

List all messages for the calling identity:

```cpp
constexpr int32 Limit = 10;
constexpr bool bForward = true;
const FString Cursor = TEXT("");
const TArray<FString> MessageIds = {};

Satori::GetMessageList(ClientConfig, Session, Limit, bForward, Cursor, MessageIds).Next(
    [](const FSatoriGetMessageListResponseResult& MessageListResult)
    {
       if (MessageListResult.bIsError)
       {
          UE_LOG(LogTemp, Error, TEXT("Error listing messages: {%d} %s"),
                 MessageListResult.Error.Code,
                 *MessageListResult.Error.Message);
          return;
       }

       for (FSatoriMessage Message : MessageListResult.Value.Messages)
       {
          UE_LOG(LogTemp, Log, TEXT("Message found: %s with text: %s"), *Message.Title,
                 *Message.Text);
       }
    });
```

### Update message status

Update the status of a message:

```cpp
const FString MessageId = TEXT("messageId");
// Both ConsumeTime and ReadTime should be Unix Timestamps
const int64 ReadTime = FDateTime::UtcNow().ToUnixTimestamp();
const int64 ConsumeTime = FDateTime::UtcNow().ToUnixTimestamp();

Satori::UpdateMessage(ClientConfig, Session, MessageId, ReadTime, ConsumeTime).Next(
    [](const FSatoriVoidResult& UpdateResult)
    {
       if (UpdateResult.bIsError)
       {
          UE_LOG(LogTemp, Error, TEXT("Error updating message: {%d} %s"),
                 UpdateResult.Error.Code, *UpdateResult.Error.Message);
       }
    });
```

### Delete message

Delete a scheduled message:

```cpp
const FString MessageId = TEXT("messageId");

Satori::DeleteMessage(ClientConfig, Session, MessageId).Next([](const FSatoriVoidResult& DeleteResult)
{
    if (DeleteResult.bIsError)
    {
       UE_LOG(LogTemp, Error, TEXT("Error deleting message: {%d} %s"), DeleteResult.Error.Code,
              *DeleteResult.Error.Message);
    }
});
```
