---
source_url: https://www.pubnub.com/docs/sdks/unreal/migration-guides/unreal-v3-migration-guide
title: Unreal SDK 3.0.0 Migration Guide
updated_at: 2026-11-04T10:18:23.000Z
sdk_name: PubNub Unreal SDK
sdk_version: 3.0.0
---

# Unreal SDK 3.0.0 Migration Guide

PubNub Unreal SDK, use the latest version: 3.0.0

> For AI agents: documentation index at https://www.pubnub.com/llms-full.txt

This guide summarizes the differences between versions 2.x and 3.0.0 and shows how to migrate to Unreal SDK 3.0.0.

Unreal SDK 3.0.0 is built on the PubNub C SDK. It replaces the previous underlying library on Windows, Linux, macOS, iOS, and Android. The release also removes the deprecated `UPubnubSubsystem` API, moves payload encryption into `FPubnubConfig`, and changes several public result types. The plugin supports Unreal Engine 5.0 through 5.8.

:::warning Breaking changes
Unreal SDK 3.0.0 contains breaking changes. Code that compiles against 2.x does not compile against 3.0.0 until you apply the changes in this guide.
:::

## What has changed

See the major differences between versions:

| Feature/Method | Unreal SDK 2.x | Unreal SDK 3.0.0 |
| --- | --- | --- |
| [Client creation](#client-creation-requires-keys-and-a-user-id) | `PublishKey` and `SubscribeKey` default to `demo`, empty `UserID` accepted | `PublishKey`, `SubscribeKey`, and `UserID` required, `CreatePubnubClient` returns `nullptr` if any is empty |
| [Automatic client](#upubnubsubsystem-manages-clients-only) | `UPubnubSubsystem` creates a default client on startup | n/a |
| [Subsystem API](#upubnubsubsystem-manages-clients-only) | Deprecated listeners, delegates, and operation methods on `UPubnubSubsystem` | `CreatePubnubClient`, `GetPubnubClient`, and `DestroyPubnubClient` only |
| [Secret key](#secret-key-user-id-and-origin-changes) | `SetSecretKeyAutomatically` and `SetSecretKey()` | `SecretKey` in `FPubnubConfig`, applied at creation. Any non-empty value gives the client admin rights. |
| [User ID](#secret-key-user-id-and-origin-changes) | `void SetUserID(FString)` | `FPubnubOperationResult SetUserID(FString)` |
| [Origin](#secret-key-user-id-and-origin-changes) | `int SetOrigin(FString)` | `Origin` in `FPubnubConfig` and `FPubnubOperationResult SetOrigin(FString)` |
| [Crypto module](#crypto-module-is-set-in-fpubnubconfig) | `SetCryptoModule()` and `GetCryptoModule()` on the client | `FPubnubConfig::CryptoModule` set before `CreatePubnubClient` |
| [Crypto provider interface](#crypto-module-is-set-in-fpubnubconfig) | `IPubnubCryptoProviderInterface` and `UPubnubCryptoBridge` | n/a |
| [Custom cryptors](#crypto-module-is-set-in-fpubnubconfig) | Any `IPubnubCryptorInterface` implementation | `UPubnubAesCryptor` and `UPubnubLegacyCryptor` only |
| [Publish method](#publish-method-and-compression-changes) | `EPubnubPublishMethod` with six values | `EPubnubPublishMethod` with `PPM_SendViaGET` and `PPM_SendViaPOST` |
| [Publish replication](#publish-method-and-compression-changes) | `FPubnubPublishSettings::Replicate` | n/a |
| [Publish compression](#publish-method-and-compression-changes) | `PPM_SendViaPOSTwithGZIP` and `PPM_UsePATCHwithGZIP` | `FPubnubPublishSettings::Compress` (`EPubnubPublishCompress`), applies only with POST |
| [List users in a channel](#presence-results-are-grouped-by-channel) | `FPubnubListUsersFromChannelResult::Data` (`FPubnubListUsersFromChannelWrapper`) | `TotalOccupancy`, `TotalChannels`, and `Channels` map of `FPubnubUsersFromChannel` |
| [Get state](#presence-results-are-grouped-by-channel) | `FPubnubGetStateResult::StateResponse` (`FString`) | `FPubnubGetStateResult::States` (`TMap<FString, FString>`) |
| [Presence async callbacks](#presence-results-are-grouped-by-channel) | `FOnPubnubListUsersFromChannelResponse` and `FOnPubnubGetStateResponse` pass separate parameters | Both pass one result struct |
| [Set state settings](#presence-results-are-grouped-by-channel) | `FPubnubSetStateSettings` has `UserID` and `HeartBeat` | Both fields removed |
| [Heartbeat](#presence-results-are-grouped-by-channel) | `Heartbeat()` and `HeartbeatAsync()` | n/a |
| [Subscribe result](#subscribe-returns-when-the-subscription-is-registered) | Result reflects the connection outcome | Result reflects that the subscription is registered, connection outcome arrives on `OnSubscriptionStatusChanged` |
| [Subscription cursor region](#subscribe-returns-when-the-subscription-is-registered) | `FPubnubSubscriptionCursor::Region` used | `Region` ignored, `FPubnubMessageData::region` always `0` |
| [Logging](#other-changes) | `UPubnubLogManager` bound to the previous library logger | Same `FPubnubLoggerConfig` and `IPubnubLoggerInterface`, internal bridge changed |
| [Token utilities](#other-changes) | `UPubnubTokenUtilities` without grant-token helpers | Existing functions unchanged, new helper functions added |
| [Unreal Engine versions](#other-changes) | 5.0 to 5.7 | 5.0 to 5.8 |

## Breaking changes

### Client creation requires keys and a user ID

In 2.x, `FPubnubConfig::PublishKey` and `FPubnubConfig::SubscribeKey` defaulted to `demo`, and the client accepted an empty `UserID`. In 3.0.0, both keys default to an empty string, and `UPubnubSubsystem::CreatePubnubClient` validates the config. If `PublishKey`, `SubscribeKey`, or `UserID` is empty, the SDK logs an error. `CreatePubnubClient` then returns `nullptr`.

`FPubnubConfig` also has new fields:

| Field | Type | Description |
| --- | --- | --- |
| `Origin` | `FString` | PubNub origin host. Leave empty to use the default origin. |
| `CryptoModule` | `TObjectPtr<UPubnubCryptoModule>` | Payload crypto module. See [Crypto module](#crypto-module-is-set-in-fpubnubconfig). |
| `PNSdkOverride` | `FString` | Internal use only. Overrides the base SDK identifier. |

The `SetSecretKeyAutomatically` field is removed. See [Secret key, user ID, and origin changes](#secret-key-user-id-and-origin-changes).

Always check the returned pointer before you use the client:

```cpp
#include "Kismet/GameplayStatics.h"
#include "Engine/GameInstance.h"
#include "PubnubSubsystem.h"

// ACTION REQUIRED: Replace ASample_Configuration with name of your Actor class
void ASample_Configuration::CreatePubnubClient()
{
	//Get PubnubSubsystem from GameInstance
	UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(this);
	UPubnubSubsystem* PubnubSubsystem = GameInstance->GetSubsystem<UPubnubSubsystem>();

	// Create Pubnub Client using Pubnub Subsystem
	FPubnubConfig PubnubConfig;
	PubnubConfig.PublishKey = TEXT("demo");   //replace with your Publish Key from Admin Portal
	PubnubConfig.SubscribeKey = TEXT("demo"); //replace with your Subscribe Key from Admin Portal
	PubnubConfig.UserID = TEXT("Player_001");
	UPubnubClient* PubnubClient = PubnubSubsystem->CreatePubnubClient(PubnubConfig);
	
	UE_LOG(LogTemp, Log, TEXT("Pubnub Client created with config"));
}
```

### UPubnubSubsystem manages clients only

Unreal SDK 3.0.0 removes the deprecated `UPubnubSubsystem` API that 2.x kept for backward compatibility. The `UPubnubSettings` class and its `InitializeAutomatically` property no longer exist, so the SDK does not create a default client at startup. The first client you create with `CreatePubnubClient` gets client ID `0`. Until you create it, `GetPubnubClient(0)` returns `nullptr`.

The subsystem now exposes only these functions:

| Function | Description |
| --- | --- |
| `CreatePubnubClient(FPubnubConfig Config, FString DebugName = "")` | Creates a client. Returns `nullptr` if the config is invalid. |
| `GetPubnubClient(int ClientID)` | Returns the client with that ID, or `nullptr`. |
| `DestroyPubnubClient(UPubnubClient* ClientToDestroy)` | Destroys the client and returns `true` if it existed. |

The following members are removed from `UPubnubSubsystem`. Call the equivalent member on `UPubnubClient` instead:

* `InitPubnub()`, `InitPubnubWithConfig()`, and `DeinitPubnub()`
* The `OnPubnubSubsystemDeinitialized`, `OnMessageReceived`, `OnMessageReceivedNative`, `OnPubnubError`, `OnPubnubErrorNative`, `OnSubscriptionStatusChanged`, and `OnSubscriptionStatusChangedNative` delegates
* All operation methods, such as `PublishMessage`, `Signal`, `SubscribeToChannel`, `SetState`, `GetState`, `GrantToken`, `RevokeToken`, and the App Context, history, and message action methods
* The 2.x delegate types without the `Pubnub` prefix, such as `FOnPublishMessageResponse`. Use the `FOnPubnub...Response` types listed in the [2.0.0 migration guide](https://www.pubnub.com/docs/sdks/unreal/migration-guides/unreal-v2-migration-guide.md#delegate-renames).

Create a client from the subsystem and keep the returned pointer. You can also look it up by ID later:

```cpp
#include "Kismet/GameplayStatics.h"
#include "Engine/GameInstance.h"
#include "PubnubSubsystem.h"

// ACTION REQUIRED: Replace ASample_Configuration with name of your Actor class
void ASample_Configuration::DestroyPubnubClient()
{
	
	UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(this);
	UPubnubSubsystem* PubnubSubsystem = GameInstance->GetSubsystem<UPubnubSubsystem>();
	
	PubnubSubsystem->DestroyPubnubClient(PubnubClient);
}
```

### Secret key, user ID, and origin changes

:::warning A secret key in FPubnubConfig now gives the client admin rights
In 2.x, `SecretKey` in `FPubnubConfig` had no effect on its own. The client used it only after you set `SetSecretKeyAutomatically` to `true` or called `SetSecretKey()`.
In 3.0.0, the client uses any non-empty `SecretKey` as soon as you call `CreatePubnubClient()`. It signs every request with the key and gets full access to the keyset, whatever the Access Manager permissions.
If your 2.x code set `SecretKey` for every client but called `SetSecretKey()` only on your server, every one of those clients gets admin rights after the upgrade. Before you upgrade, check every place you build an `FPubnubConfig`. Leave `SecretKey` empty for every client that isn't a trusted server.
:::

The SDK now applies `SecretKey` and `Origin` only when it creates the client:

* `SecretKey` is read from `FPubnubConfig`. The `SetSecretKeyAutomatically` field and the `UPubnubClient::SetSecretKey()` method are removed. You can't change the secret key after the client exists. Use a secret key only in server or backend clients.
* `Origin` is read from `FPubnubConfig`. You can still change it later with `SetOrigin`.
* `SetUserID` now returns `FPubnubOperationResult` instead of `void`. An empty user ID returns an error result.
* `SetOrigin` now returns `FPubnubOperationResult` instead of `int`. The old return codes `0`, `1`, and `-1` no longer exist. A successful call has `Status` 200 and `Error` set to `false`. An empty origin resets the client to the default origin.
* `GetOrigin` returns the origin currently in use, including the default origin when you did not set one.

Update code that compares the old `int` result of `SetOrigin` to check `FPubnubOperationResult::Error` instead.

### Crypto module is set in FPubnubConfig

In 2.x, you called `UPubnubClient::SetCryptoModule` after you created the client. In 3.0.0, you build a `UPubnubCryptoModule`, assign it to `FPubnubConfig::CryptoModule`, and then call `CreatePubnubClient`. The client uses the module for publish, subscribe, history, and files. Keep the module alive for the life of the client.

These APIs are removed:

* `UPubnubClient::SetCryptoModule()` and `UPubnubClient::GetCryptoModule()`
* `IPubnubCryptoProviderInterface`, with its `ProviderEncrypt` and `ProviderDecrypt` functions
* `UPubnubCryptoBridge`

`UPubnubCryptoModule` no longer implements a provider interface, and `InitCryptoModule` now returns `bool`:

* Each cryptor must have its cipher key set before you call `InitCryptoModule`.
* `InitCryptoModule` returns `false` if a live client still holds the module.
* The module accepts at most four additional cryptors.
* The module accepts only `UPubnubAesCryptor` and `UPubnubLegacyCryptor`. It rejects custom `IPubnubCryptorInterface` implementations, even though the interface itself is still source-compatible.
* `IPubnubCryptorInterface` keeps `GetIdentifier`, `Encrypt`, and `Decrypt`. The SDK calls them through the built-in cryptors only.

`UPubnubAesCryptor` and `UPubnubLegacyCryptor` also change. Call `SetCipherKey` before you use the cryptor. The SDK seals the cryptor when a crypto module takes it, so you can't change the key afterwards. For `UPubnubLegacyCryptor`, set `UseRandomIV` before you call `InitCryptoModule`.

The first snippet creates a module with the AES cryptor and assigns it to the config. The second adds the legacy cryptor, which decrypts data from other SDKs:

```cpp
// ACTION REQUIRED: Replace ASample_Crypto with name of your Actor class
void ASample_Crypto::SetCryptoModuleSample()
{
	
	// Crypto is assigned on FPubnubConfig before the client is created.
	UPubnubAesCryptor* AesCryptor = NewObject<UPubnubAesCryptor>(this);
	AesCryptor->SetCipherKey("enigma");

	UPubnubCryptoModule* CryptoModule = NewObject<UPubnubCryptoModule>(this);
	CryptoModule->InitCryptoModule(AesCryptor, {});

	UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(this);
	UPubnubSubsystem* PubnubSubsystem = GameInstance->GetSubsystem<UPubnubSubsystem>();
	FPubnubConfig Config;
	Config.PublishKey = TEXT("demo");
	Config.SubscribeKey = TEXT("demo");
	Config.UserID = TEXT("player_001");
	Config.CryptoModule = CryptoModule;
	PubnubClient = PubnubSubsystem->CreatePubnubClient(Config);
}
```

```cpp
#include "Crypto/PubnubLegacyCryptor.h"

// ACTION REQUIRED: Replace ASample_Crypto with name of your Actor class
void ASample_Crypto::SetCryptoModuleWithLegacySample()
{
	
	// Crypto is assigned on FPubnubConfig before the client is created.
	UPubnubAesCryptor* AesCryptor = NewObject<UPubnubAesCryptor>(this);
	AesCryptor->SetCipherKey("enigma");

	// Legacy Cryptor is only needed for compatibility with SDKs that already use Legacy encryption.
	UPubnubLegacyCryptor* LegacyCryptor = NewObject<UPubnubLegacyCryptor>(this);
	LegacyCryptor->SetCipherKey("enigma");

	UPubnubCryptoModule* CryptoModule = NewObject<UPubnubCryptoModule>(this);
	CryptoModule->InitCryptoModule(AesCryptor, {LegacyCryptor});

	UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(this);
	UPubnubSubsystem* PubnubSubsystem = GameInstance->GetSubsystem<UPubnubSubsystem>();
	FPubnubConfig Config;
	Config.PublishKey = TEXT("demo");
	Config.SubscribeKey = TEXT("demo");
	Config.UserID = TEXT("player_001");
	Config.CryptoModule = CryptoModule;
	PubnubClient = PubnubSubsystem->CreatePubnubClient(Config);
}
```

For a complete example that publishes and receives encrypted messages, see the full crypto sample:

```cpp
#include "Samples/Sample_CryptoFull.h"
#include "Kismet/GameplayStatics.h"
#include "Engine/GameInstance.h"
#include "Crypto/PubnubAesCryptor.h"
#include "Crypto/PubnubCryptoModule.h"
#include "PubnubSubsystem.h"
#include "PubnubClient.h"

void ASample_CryptoFull::BeginPlay()
{
	Super::BeginPlay();

	//Run the example on BeginPlay
	RunCryptoFullExample();
}

void ASample_CryptoFull::RunCryptoFullExample()
{
	//Get PubnubSubsystem from GameInstance
	UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(this);
	UPubnubSubsystem* PubnubSubsystem = GameInstance->GetSubsystem<UPubnubSubsystem>();
	
	//Create Pubnub Client using Pubnub Subsystem
	FPubnubConfig Config;
	Config.PublishKey = TEXT("demo");   //replace with your Publish Key from Admin Portal
	Config.SubscribeKey = TEXT("demo"); //replace with your Subscribe Key from Admin Portal
	Config.UserID = TEXT("Player_001");

	// Crypto is fixed when the client is created. It encrypts published messages and decrypts received and history messages.
	UPubnubAesCryptor* AesCryptor = NewObject<UPubnubAesCryptor>(this);
	AesCryptor->SetCipherKey("enigma");
	UPubnubCryptoModule* CryptoModule = NewObject<UPubnubCryptoModule>(this);
	CryptoModule->InitCryptoModule(AesCryptor, {});
	Config.CryptoModule = CryptoModule;
	PubnubClient = PubnubSubsystem->CreatePubnubClient(Config);

	UE_LOG(LogTemp, Log, TEXT("Crypto example, Crypto Module is set"));

	//Add Listener/Delegate that will broadcast whenever message is received on any subscribed channel or group
	PubnubClient->OnMessageReceived.AddDynamic(this, &ASample_CryptoFull::OnPubnubMessageReceived);

	//Subscribe to the Channel
	FString Channel = TEXT("secret_guild_chat");
	PubnubClient->SubscribeToChannelAsync(Channel);

	// NOTE: Subscribing to a group or channel may take a few seconds to complete.
	// This sleep is used to simulate the waiting period in an actual application.
	FPlatformProcess::Sleep(3);
	
	UE_LOG(LogTemp, Log, TEXT("Crypto example, subscribed to channel: %s"), *Channel);
		
	//Bind delegate to the publish result
	FOnPubnubPublishMessageResponse OnPublishMessageResponse;
	OnPublishMessageResponse.BindDynamic(this, &ASample_CryptoFull::OnPublishResult);
	
	//Publish message to the subscribed channel - this message will be encrypted automatically because crypto module is set
	FString Message = R"({"event": "PowerUpUsed", "powerup": "Invisibility Cloak", "duration": 10})";
	PubnubClient->PublishMessageAsync(Channel, Message, OnPublishMessageResponse);

	// NOTE: Give some time to receive message before unsubscribing
	// This sleep is used only to simulate the waiting period in an actual application.
	FPlatformProcess::Sleep(3);
	
	//Unsubscribe from previously subscribed channel
	PubnubClient->UnsubscribeFromChannelAsync(Channel);

	UE_LOG(LogTemp, Log, TEXT("Crypto example, message published"));
}

void ASample_CryptoFull::OnPubnubMessageReceived(FPubnubMessageData Message)
{
	UE_LOG(LogTemp, Log, TEXT("Crypto example, message received on Channel: %s, Message Content: %s"), *Message.Channel, *Message.Message);
}

void ASample_CryptoFull::OnPublishResult(FPubnubOperationResult Result, FPubnubMessageData Message)
{
	if(Result.Error)
	{
		UE_LOG(LogTemp, Error, TEXT("Crypto example, failed to publish message. Status: %d, Reason: %s"), Result.Status, *Result.ErrorMessage);
	}
	else
	{
		UE_LOG(LogTemp, Log, TEXT("Crypto example, message published successfully. Published message timetoken: %s"), *Message.Timetoken);
	}
}
```

### Publish method and compression changes

`EPubnubPublishMethod` now has two values:

| Value | Description |
| --- | --- |
| `PPM_SendViaGET` | The message is part of the URL path. There is no body, so compression does not apply. This is the default. |
| `PPM_SendViaPOST` | The message is part of the request body. Required for compression. |

The values `PPM_UsePATCH`, `PPM_SendViaPOSTwithGZIP`, `PPM_UsePATCHwithGZIP`, and `PPM_UseDELETE` are removed. The `FPubnubPublishSettings::Replicate` field is removed, so you can no longer publish a message that only Functions receive.

Gzip compression moves into a new `FPubnubPublishSettings::Compress` field of the new `EPubnubPublishCompress` enum:

| Value | Description |
| --- | --- |
| `PPC_No` | Sends the request body uncompressed. This is the default. |
| `PPC_Yes` | Gzips the request body and sends `Content-Encoding: gzip`. The SDK ignores it unless `PublishMethod` is `PPM_SendViaPOST`. |

`PPC_Yes` does not switch a GET publish to POST. To replace `PPM_SendViaPOSTwithGZIP`, set `PublishMethod` to `PPM_SendViaPOST` and `Compress` to `PPC_Yes`. To replace `PPM_UsePATCHwithGZIP`, use the same combination, because PATCH is no longer available.

The 3.0.0 samples do not include a snippet for `Compress`. The following snippet shows how to pass `FPubnubPublishSettings` to a publish call. Set the `PublishMethod` and `Compress` fields on the same struct:

```cpp
// ACTION REQUIRED: Replace ASample_PubSub with name of your Actor class
void ASample_PubSub::PublishWithSettingsSample()
{
	
	//Assumes PubnubClient is created and UserID is set

	//Set Channel and Message to Publish
	FString Channel = TEXT("global_chat");
	FString JsonMessage = R"({"event": "PowerUpUsed", "powerup": "Invisibility Cloak", "duration": 10})";

	//Create additional PublishSettings
	FPubnubPublishSettings PublishSettings;
	PublishSettings.MetaData = R"({"map": "DesertStrike", "match_id": "MATCH-42"})";
	PublishSettings.CustomMessageType = "game-event";

	//Publish message with settings
	PubnubClient->PublishMessageAsync(Channel, JsonMessage, PublishSettings);
}
```

### Presence results are grouped by channel

Presence reads now return data keyed by channel name, and `Heartbeat` is removed.

#### ListUsersFromChannel

In 2.x, `FPubnubListUsersFromChannelResult::Data` held an `FPubnubListUsersFromChannelWrapper` with `Occupancy` and a `UsersState` map of user ID to state. In 3.0.0, the wrapper is gone and the result struct has three fields:

| Field | Type | Description |
| --- | --- | --- |
| `TotalOccupancy` | `int` | Occupancy across all returned channels. |
| `TotalChannels` | `int` | Number of channels the server reports. |
| `Channels` | `TMap<FString, FPubnubUsersFromChannel>` | Users present on each returned channel, keyed by channel name. |

Each `FPubnubUsersFromChannel` has `Channel`, `Occupancy`, and `Users`, which is a `TArray<FPubnubUserFromChannel>`. Each `FPubnubUserFromChannel` has `UserID` and `State`. `State` is raw state JSON, empty if you did not request state or the user has none. `Occupancy` can be larger than `Users.Num()` when `Limit` and `Offset` paginate the list.

The async delegates `FOnPubnubListUsersFromChannelResponse` and `FOnPubnubListUsersFromChannelResponseNative` now take one `FPubnubListUsersFromChannelResult` parameter instead of `FPubnubOperationResult` and `FPubnubListUsersFromChannelWrapper`. Read the operation status from `Result.Result`.

```cpp
// ACTION REQUIRED: Replace ASample_Presence with name of your Actor class
void ASample_Presence::ListUsersFromChannelSample()
{
	
	//Assumes PubnubClient is created and UserID is set

	// Bind response delegate
	// ACTION REQUIRED: Replace ASample_Presence with name of your Actor class
	FOnPubnubListUsersFromChannelResponse OnListUsersFromChannelResponse;
	OnListUsersFromChannelResponse.BindDynamic(this, &ASample_Presence::OnListUsersFromChannelResponse_Simple);

	//List users from a channel
	FString Channel = TEXT("guild-channel");
	PubnubClient->ListUsersFromChannelAsync(Channel, OnListUsersFromChannelResponse);
}

// ACTION REQUIRED: Replace ASample_Presence with name of your Actor class
void ASample_Presence::OnListUsersFromChannelResponse_Simple(FPubnubListUsersFromChannelResult Result)
{
	if(Result.Result.Error)
	{
		UE_LOG(LogTemp, Error, TEXT("Failed to list users from channel. Status: %d, Reason: %s"), Result.Result.Status, *Result.Result.ErrorMessage);
	}
	else
	{
		UE_LOG(LogTemp, Log, TEXT("Users successfully listed from channel. Occupancy: %d"), Result.TotalOccupancy);
	}
}
```

#### GetState

In 2.x, `FPubnubGetStateResult::StateResponse` was one JSON `FString`. In 3.0.0, `States` replaces it. `States` is a `TMap<FString, FString>` of channel name to raw state JSON. The string is empty when the user has no state on that channel. The async delegates `FOnPubnubGetStateResponse` and `FOnPubnubGetStateResponseNative` now take one `FPubnubGetStateResult` parameter instead of `FPubnubOperationResult` and `FString`.

```cpp
// ACTION REQUIRED: Replace ASample_Presence with name of your Actor class
void ASample_Presence::GetStateSample()
{
	
	//Assumes PubnubClient is created and UserID is set

	// Bind response delegate
	// ACTION REQUIRED: Replace ASample_Presence with name of your Actor class
	FOnPubnubGetStateResponse OnGetStateResponse;
	OnGetStateResponse.BindDynamic(this, &ASample_Presence::OnGetStateResponse_Simple);

	//Get state for the user on a channel
	FString Channel = TEXT("presence-channel");
	FString ChannelGroup = TEXT("");
	PubnubClient->GetStateAsync(Channel, ChannelGroup, UserID, OnGetStateResponse);
}

// ACTION REQUIRED: Replace ASample_Presence with name of your Actor class
void ASample_Presence::OnGetStateResponse_Simple(FPubnubGetStateResult Result)
{
	if(Result.Result.Error)
	{
		UE_LOG(LogTemp, Error, TEXT("Failed to get state. Status: %d, Reason: %s"), Result.Result.Status, *Result.Result.ErrorMessage);
	}
	else
	{
		const FString* State = Result.States.Find(TEXT("presence-channel"));
		UE_LOG(LogTemp, Log, TEXT("Successfully got state: %s"), State ? **State : TEXT(""));
	}
}
```

#### SetState and Heartbeat

* `FPubnubSetStateSettings::UserID` and `FPubnubSetStateSettings::HeartBeat` are removed. `SetState` always sets state for the client's current user ID, and the settings keep `ChannelGroup`.
* `UPubnubClient::Heartbeat()` and `UPubnubClient::HeartbeatAsync()` are removed. The SDK has no replacement.

### Subscribe returns when the subscription is registered

`SubscribeToChannel` and `SubscribeToGroup`, including the `Async` variants, now report that the SDK registered the subscription locally. They no longer wait for the connection to complete. The returned `FPubnubOperationResult` (or the callback result) does not tell you whether the connection succeeded. Listen to `OnSubscriptionStatusChanged` on the client for connection success and failure. If your code treated a successful subscribe result as a confirmed connection, move that logic into the status listener.

```cpp
// ACTION REQUIRED: Replace ASample_Configuration with name of your Actor class
void ASample_Configuration::SubscriptionStatusListenerSample()
{
	
	//Assumes PubnubClient is created and UserID is set

	//Bind delegate to the subscription status listener
	// ACTION REQUIRED: Replace ASample_Configuration with name of your Actor class
	PubnubClient->OnSubscriptionStatusChanged.AddDynamic(this, &ASample_Configuration::OnSubscriptionStatusChanged);
}

// ACTION REQUIRED: Replace ASample_Configuration with name of your Actor class
void ASample_Configuration::OnSubscriptionStatusChanged(EPubnubSubscriptionStatus Status, FPubnubSubscriptionStatusData StatusData)
{
	//Do something with changed status, for example print whenever there is connection error
	if(Status == EPubnubSubscriptionStatus::PSS_ConnectionError || Status == EPubnubSubscriptionStatus::PSS_DisconnectedUnexpectedly)
	{
		UE_LOG(LogTemp, Warning, TEXT("Something went wrong with subscription. Reason: %s"), *StatusData.Reason);
	}
}
```

The subscription cursor also changes:

* `FPubnubSubscriptionCursor::Region` is ignored. Resuming uses `Timetoken` only, and the SDK logs a warning if you set a non-zero `Region`.
* `FPubnubMessageData::region` is always `0`.

### Other changes

These changes affect fewer projects:

* **Logging.** `FPubnubLoggerConfig`, `IPubnubLoggerInterface`, and `DefaultLoggerMinCCoreLevel` keep their names and behavior, now applied to logs from the PubNub library itself. If you call `UPubnubLogManager` directly, `GetCCoreLoggerInterface()` is removed. The class now has `GetCCoreLoggerProvider()` and `ReleaseCCoreLoggerBridge()`, and `HandleCCoreLog` takes a different entry type. These are internal functions, so most projects are not affected.
* **Token utilities.** The existing `UPubnubTokenUtilities` functions keep their signatures. 3.0.0 adds the public static functions `BuildReworkedParsedToken`, `BuildGrantTokenResourceStorage`, and `ApplyGrantTokenResourceStorage`. The SDK uses them in its own grant and parse code. You don't need to call them.
* **Utility functions.** The public `UPubnubUtilities` header no longer declares `PubnubConfigFromPluginSettings`, `PubnubGetLastServerHttpResponse`, `AddQuotesToString`, `RemoveOuterQuotesFromString`, `PubnubCharMemBlockToString`, `ArrayOfStringsToCommaSeparatedString`, `SafeCopyFStringToCharBuffer`, and `UEMessageFromPubnubMessage`. Remove any calls to them.
* **Internal client API.** `SetRuntimeSdkVersionSuffix` is removed from `UPubnubClient`.
* **Subscription sets.** `UPubnubSubscriptionSet` has a new private initializer that accepts channel metadata and user metadata IDs. Your code does not call it.
* **Unreal Engine versions.** The plugin supports Unreal Engine 5.0 through 5.8.

## Migration steps

To migrate from Unreal SDK 2.x to 3.0.0:

1. Update your PubNub plugin to version 3.0.0.
2. Create the client explicitly with valid config values:

   | Action | Description |
   | --- | --- |
   | Set `PublishKey`, `SubscribeKey`, and `UserID` on `FPubnubConfig` | The SDK no longer defaults the keys to `demo`, and `CreatePubnubClient` returns `nullptr` if any value is empty. |
   | Call `UPubnubSubsystem::CreatePubnubClient()` at startup and check the returned pointer | The SDK does not create a client for you, and `GetPubnubClient(0)` returns `nullptr` until you create one. |
   | Remove uses of `UPubnubSettings`, `InitPubnub()`, `InitPubnubWithConfig()`, and `DeinitPubnub()` | These APIs no longer exist. |

3. Move off the removed `UPubnubSubsystem` API:

   | Action | Description |
   | --- | --- |
   | Call operations and bind delegates on `UPubnubClient` | `UPubnubSubsystem` keeps only `CreatePubnubClient`, `GetPubnubClient`, and `DestroyPubnubClient`. |
   | Replace delegate types such as `FOnPublishMessageResponse` with `FOnPubnubPublishMessageResponse` | Delegates without the `Pubnub` prefix are removed. |

4. Update secret key, user ID, and origin handling:

   | Action | Description |
   | --- | --- |
   | Find every `FPubnubConfig` with `SecretKey` set | In 3.0.0, a non-empty `SecretKey` always gives the client admin rights. Keep it only on clients where 2.x called `SetSecretKey()` or set `SetSecretKeyAutomatically`. |
   | Remove `SetSecretKeyAutomatically` and `SetSecretKey()` calls | Do this only after the check above. |
   | Set `Origin` in `FPubnubConfig` | It applies only at creation. Use `SetOrigin()` to change it later. |
   | Check `FPubnubOperationResult` from `SetUserID()` and `SetOrigin()` | `SetUserID()` returned `void` and `SetOrigin()` returned `int` in 2.x. |

5. Move crypto into the config:

   | Action | Description |
   | --- | --- |
   | Create the cryptors, set their cipher keys, and call `InitCryptoModule()` | Check that it returns `true`. |
   | Assign the module to `FPubnubConfig::CryptoModule` before `CreatePubnubClient()` | Remove `SetCryptoModule()` and `GetCryptoModule()` calls. |
   | Replace custom cryptors with `UPubnubAesCryptor` or `UPubnubLegacyCryptor` | The crypto module rejects other implementations. |
   | Remove code that uses `IPubnubCryptoProviderInterface` or `UPubnubCryptoBridge` | Both are removed. |

6. Update publish settings:

   | Action | Description |
   | --- | --- |
   | Remove `FPubnubPublishSettings::Replicate` | The field no longer exists. |
   | Replace removed `EPubnubPublishMethod` values | Use `PPM_SendViaGET` or `PPM_SendViaPOST`. |
   | Set `PublishMethod` to `PPM_SendViaPOST` and `Compress` to `PPC_Yes` to gzip the body | `Compress` has no effect on GET publishes. |

7. Update presence code:

   | Action | Description |
   | --- | --- |
   | Read `ListUsersFromChannel` results from `TotalOccupancy`, `TotalChannels`, and `Channels` | The `Data` field and `FPubnubListUsersFromChannelWrapper` are removed. |
   | Read `GetState` results from `States` by channel name | The `StateResponse` field is removed. |
   | Update `ListUsersFromChannelAsync` and `GetStateAsync` callbacks | Both now receive one result struct. |
   | Remove `Heartbeat()`, `HeartbeatAsync()`, and the `UserID` and `HeartBeat` fields of `FPubnubSetStateSettings` | They no longer exist. |

8. Update subscribe handling:

   | Action | Description |
   | --- | --- |
   | Treat a successful `SubscribeToChannel()` or `SubscribeToGroup()` result as "subscription registered" | The result no longer confirms a connection. |
   | Bind `OnSubscriptionStatusChanged` to learn about connection success and failure | Connection outcomes arrive there. |
   | Stop relying on `FPubnubSubscriptionCursor::Region` and `FPubnubMessageData::region` | The SDK ignores the cursor region and always sets the message region to `0`. |

9. Test your application. Pay special attention to client creation, encrypted publish and subscribe, presence reads, and subscription status handling.

## Additional resources

For API details, see the [Unreal SDK documentation](https://www.pubnub.com/docs/sdks/unreal.md). For questions or issues, contact [PubNub support](mailto:support@pubnub.com).

Last updated at: 2026-11-04T10:18:23.000Z
