Unreal Introduction

The Unreal SDK enables you to build any type of chat or messaging experience for Android, iOS, Windows, macOS or Linux. Support for more platforms is coming soon!

The SDK is implemented as an Unreal Engine Plugin, which includes low-level client access to the Stream Chat service as well as an early preview of some UI widgets which are ready to be dropped into your project. The online API tour shows how the API works; it runs JavaScript in the browser, but the ideas are the same in Unreal.

Start here

This SDK is the client half of your integration. App settings, permissions, webhooks, push templates and data retention are configured from your backend with a server-side SDK or the REST API, and the token your app connects with has to be signed there too. See the server-side overview for what belongs there.

Build with Stream CLI and Agent Skills

The Stream CLI and Agent Skills give AI coding agents the tools and knowledge to work with Stream. There is no dedicated skill pack for Unreal yet, but the default skills still help with live documentation and CLI-driven API work.

Install the CLI and the default skills:

curl -fsSL https://getstream.io/cli.sh | bash
getstream skills

Once installed, invoke /stream from your agent. It routes documentation lookups and API operations for you.

Stream Agent Skills can also be installed from skills.sh.

Requirements

Unreal Engine5.7, 5.8
PlatformsWindows (Win64), macOS, Linux, Android, iOS
LanguageC++ and Blueprint. Some lower-level and pagination APIs are C++ only.

Only the engine versions exercised by the SDK's CI and release matrix are listed as supported.

Plugin version 2.0.0 dropped support for UE 4.27, 5.0 and 5.1. Those three engine versions are covered by v1.3.0, the last release that supports them, and no release supports 5.2 through 5.6. If your project is on one of those, plan an engine upgrade to 5.7 or 5.8 before adopting 2.0.0.

Feature support

The Unreal SDK is in beta and does not cover the whole Chat API yet. Its surface may still change between releases, and C++ and Blueprint coverage can differ per operation.

Available today:

  • Messaging: send, edit, fetch, full-text search, soft delete through the channel API, hard delete through the lower-level C++ API, and pagination in both directions from C++
  • Attachments: file and image uploads, deletion, and attachments on sent and received messages. Images are rendered inline by the message list widget, and other files as a titled row
  • Threads: reply in a thread, optionally showing the reply in the channel too, paginate a thread's replies, and query the threads a user takes part in
  • Channels: query, watch, create, update, truncate, hide and show, freeze, and member management
  • Reactions: send with score and enforce-unique semantics, remove, and paginate from C++
  • Read state: mark read, mark all read, unread counts, and per-user read tracking
  • Typing indicators: keystroke debouncing and stop events
  • Moderation: ban, shadow ban, mute users, mute channels, block and unblock users, flag messages and users, and query banned users
  • Presence: online state, watchers, and channel own capabilities
  • Real-time events: WebSocket transport with health checks and reconnection, plus typed event subscription from C++ and Blueprint
  • Slow mode with a configurable cooldown, and push notification device registration
  • UI widgets: an early preview of channel list, message list, composer, attachment rendering, threads, reactions and avatar widgets. Long pressing a message opens the reaction picker and the message actions, which is how reactions and actions are reached on a touch screen

Not implemented in Unreal yet, so the pages in these docs that describe them do not apply to this SDK:

  • A thread list screen, thread unread counts, and watching threads
  • Quoted messages, mentions, and pinning messages
  • Offline persistence and optimistic sending
  • Channel archiving and channel pinning
  • Polls, draft messages, message reminders, and location sharing

Open an issue if you need one of these.

Getting started

This guide will quickly help you get up to speed on Stream’s Chat API. The API is flexible and allows you to build any type of chat or messaging application.

First, you want to make sure you've downloaded the latest release of the Stream Chat plugin from the Releases page of the GitHub repository and copied it to the Plugins directory of your project.

Next, make sure you have the Stream Chat plugin enabled in the Plugins panel of your project.

The GitHub releases page is the recommended way to install the plugin. If you previously acquired it through the Epic Games Launcher, it stays available in your Vault, but new projects should start from the release above.

Set n.VerifyPeer before you package

If you are going to package for iOS or Android, add this now. Without it the packaged app cannot connect to Stream at all, and the error does not point at the cause.

Add to Config/DefaultEngine.ini:

[/Script/Engine.NetworkSettings]
n.VerifyPeer=True

The SDK talks to Stream over a secure WebSocket, so it needs a bundle of CA root certificates to verify Stream's server certificate. Unreal ships one at Engine/Content/Certificates/ThirdParty/cacert.pem, and the editor reads it straight from disk, which is why chat works in the editor whatever you do here.

Packaging is where it goes wrong. Whether that bundle is copied into your build is gated on n.VerifyPeer, and the check does not behave the way its default suggests:

// CopyBuildToStagingDirectory.Automation.cs
bool bStageSSLCertificates = true;
PlatformEngineConfig.GetBool("/Script/Engine.NetworkSettings", "n.VerifyPeer", out bStageSSLCertificates);

GetBool writes to the out parameter even when the key is missing, so leaving it unset does not preserve that true. It replaces it with false and no certificates are staged. Nothing sets a default, so every new project starts in that state, and the packaged app has no way to verify any TLS connection:

LogChatSocket: Initiating WebSocket connection
LogWebSockets: Warning: Lws(Error): SSL error: unable to get local issuer certificate (preverify_ok=0;err=20;depth=2)
LogChatSocket: Error: Failed to connect to WebSocket [Error=client connect failed]
LogChatSocket: Enqueuing a reconnecting attempt [Attempt=1, Delay=1.02866]

The client then retries with backoff indefinitely. It reads like a network, token or firewall problem rather than a missing file.

Setting n.VerifyPeer=True does not make your app less strict. At runtime the value only takes effect if the key is present, so an unset key already means "verify the peer". Setting it explicitly just makes packaging agree with the behaviour you already had.

To confirm the bundle made it into a staged build:

strings -a Saved/StagedBuilds/IOS/*/cookeddata/*/content/paks/*.pak | grep cacert

You do not need to copy cacert.pem into your project. Unreal stages its own copy. If you do want to pin a specific bundle, put it at Content/Certificates/cacert.pem and it is staged in preference to the engine's.

Chat client

You'll need to place the StreamChatClientComponent ActorComponent on one of the Actors of your project. Recommended places include your HUD or GameState actors.

// MyHud.h

UCLASS()
class AStreamChatSampleHud final : public AHUD
{
  GENERATED_BODY()

public:
  AStreamChatSampleHud();
  virtual void BeginPlay() override;

  UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
  UStreamChatClientComponent* Client;
};
// MyHud.cpp

AStreamChatSampleHud::AStreamChatSampleHud()
{
  // Create a new instance of [UStreamChatClientComponent] passing the api_key obtained
  //from your project dashboard.
  Client = CreateDefaultSubobject<UStreamChatClientComponent>(TEXT("Client"));
  Client->ApiKey = TEXT("{{ api_key }}");
}

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

  /// Set the current user and connect the websocket.
  /// In a production scenario, this should be done using a backend to generate
  /// a user token using our server SDK.
  /// Please see the following for more information:
  /// https://getstream.io/chat/docs/unreal/tokens-and-authentication/
  const FUser User{TEXT("super-band-9")};
  const FString Token{TEXT("{{ chat_user_token }}")};
  Client->ConnectUser(
    User,
    Token,
    [](const FUserRef& UserRef)
    {
      // Successfully connected
    });
}

The user token is typically provided by your backend when you login or register in the app. If authentication is disabled for your app you can also use a Client->DevToken(UserId) to generate an insecure token for development. You should never launch into production with authentication disabled.

For more complex token generation and expiration examples have a look at the token provider documentation.

Channels

Let’s continue by watching your first channel. A channel contains messages and a list of members who are permanently associated with the channel and a list of watchers currently watching the channel. The example below shows how to set up a channel:

FChannelProperties Props = FChannelProperties::WithType(TEXT("messaging"));
Props.SetId(TEXT("unrealdevs"));
Props.ExtraData.SetString(TEXT("name"), TEXT("Unreal devs"));

Props.ExtraData.SetNumber(TEXT("my_custom_number"), 123);
Client->WatchChannel(
  Props,
  [](UChatChannel* Channel)
  {
    // Started watching channel
  });

We construct a FChannelProperties struct with a Channel Type and a Channel ID ( messaging and unrealdevs in this case). The Channel ID is optional; if you leave it out, the ID is determined based on the list of members, which can be set instead using SetMembers() .

The Channel Type controls the settings we’re using for this channel.

There are 5 default types of channels:

  • livestream

  • messaging

  • team

  • gaming

  • commerce

These five options above provide you with the most suitable defaults for their use cases. You can also define custom channel types if one of the defaults don’t work for your use case.

The channel properties struct also contains a member ExtraData which contains the extra channel data. You can add as many custom fields as you would like as long as the total size of the resulting JSON object is less than 5 KB. Here we set the name field which is used by convention for the human-readable name of the channel.

Messages

Now that we have the channel set up, let's send our first chat message:

FMessage Message{TEXT("I told them I was pesca-pescatarian. Which is one who eats solely fish who eat other fish.")};
Message.ExtraData.SetNumber(TEXT("custom_field"), 123);
Channel->SendMessage(Message);

Similar to users and channels, the SendMessage method allows you to add custom fields. When you send a message to a channel, Stream Chat automatically broadcasts to all the people that are watching this channel and updates in real-time.

Attachments

Uploading and sending are separate steps. Upload the bytes, then put the attachment you get back onto a message and send that.

Channel->UploadImage(
  TEXT("shot.png"),
  ImageBytes,
  [Channel](const FAttachment& Attachment)
  {
    FMessage Message{TEXT("Look at this")};
    Message.Attachments.Add(Attachment);
    Channel->SendMessage(Message);
  });

The SDK takes bytes rather than a file path. Choosing a file is a platform decision, a photo library on a handset and a file dialog on desktop, so the plugin stays portable by leaving that to your app.

The message list renders whatever arrives: images inline, and any other file as a row naming it. To let people attach something from the composer, register a picker. The composer then grows an attach button; register nothing and it does not.

#include "Input/AttachmentPicker.h"

FAttachmentPicker::SetProvider(
  [](FAttachmentPicker::FOnPicked OnPicked)
  {
    // Show your own picker, then hand back whatever the user chose
    FPickedAttachment Picked;
    Picked.FileName = TEXT("shot.png");
    Picked.Content = MoveTemp(Bytes);
    Picked.bIsImage = true;
    OnPicked(Picked);

    // Or OnPicked({}) if they cancelled
  });

The composer uploads what the picker hands back and sends it with the next message, so a message carrying an attachment and no text is valid. The callback is always delivered on the game thread, whichever thread your picker calls it from.

The sample app implements this for iOS with a PHPickerViewController in Source/StreamChatSample/IOSAttachmentPicker.mm, which is also the only platform-specific code in the project. See Image and File Uploads for deleting uploads, and for resizing images on the fly.

Threads

Replying to a message starts a thread on it. A reply belongs to its thread and stays out of the channel, unless you ask for it to appear in both.

Channel->SendReply(FMessage{TEXT("Hello world")}, ParentMessage);

// Show it in the channel as well
Channel->SendReply(FMessage{TEXT("Hello world")}, ParentMessage, true);

A channel query returns the channel's history, not its threads, so a thread's replies are fetched when the user opens it.

Channel->QueryReplies(ParentMessage, 20);

// Page back through older replies as the user scrolls up the thread
Channel->QueryAdditionalReplies(ParentMessage, 20);

The replies are kept on the channel at Channel->GetReplies(ParentMessage), and RepliesUpdated fires with the parent message id whenever they change. A reply is recognised by its ParentId rather than its type, because the API reports thread replies as regular messages, so FMessage::IsThreadReply() is the check to use.

To list the threads a user takes part in, across all of their channels, ask the client rather than a channel.

Client->QueryThreads(
  FFilter::Equal(TEXT("has_unread"), true),
  {},    // Sort options
  10,    // Threads per page
  2,     // Replies to preview per thread
  {},    // Page token from a previous response
  [](const TArray<FChatThread>& Threads, const FString& NextPage)
  {
    // One entry per thread, each with its parent message and a preview of its replies
  });

In the sample app, long pressing a message opens the reaction picker and the message actions, including Reply in thread. A message that has replies shows the count underneath, which opens the thread. See Threads and Replies for the rest of the API.

Querying channels

The Client->QueryChannels method enables you to retrieve a list of channels. You can specify a filter and sort order. The client keeps the channels list updates as new messages, reactions and new channels arrive.

const FFilter Filter = FFilter::And({
  FFilter::In(TEXT("members"), {TEXT("thierry")}),
  FFilter::Equal(TEXT("type"), TEXT("messaging")),
});
const TArray<FChannelSortOption> SortOptions{{EChannelSortField::LastMessageAt, ESortDirection::Descending}};
Client->QueryChannels(
  [](const TArray<UChatChannel*> ReceivedChannels)
  {
    // Started watching channels
  },
  Filter,
  SortOptions);

To learn more about which fields you can query and sort on have a look at the query channels documentation.

What's next

Now that you understand the building blocks of a fully-functional chat integration, these are good places to go deeper: