---
title: "Relationships and activity invites"
description: "Show activity, manage relationships, and invite players into a party."
seo:
  noindex: true
---

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

[Sign in](/integrations/discord/setup-login) and wait for **Ready** before using social features. Discord-wide friends and game friends are separate relationships.

With the crossplay kit, **Send Crossplay Lobby Invite** to a Discord friend sends the activity invite for you and an accepted invite joins the crossplay lobby; see [parties and invites](/crossplay/parties-invites).

## Publish a joinable activity

**Blueprint**

<BlueprintViewer title="Publish a joinable activity" src="/blueprints/crossplay-discord-presence.txt" />

Game Instance variables: `PresenceBusy`, `ClearPresenceRequested` (**Boolean**, false), `PendingPresenceOperation` (**String**, empty).

**C++**

Game module dependencies: `DiscordGame`, `BetideCore`, `Engine`.

```cpp
// Copyright (c) 2026 Betide Studio. All Rights Reserved.
// DiscordPresenceGameInstance.h
#pragma once
#include "Engine/GameInstance.h"
#include "DiscordGameSubsystem.h"
#include "DiscordPresenceGameInstance.generated.h"

UCLASS()
class UDiscordPresenceGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    UFUNCTION(BlueprintCallable)
    void PublishDiscordParty(const FString &PartyId, const FString &JoinSecret, int32 Members,
                             int32 Capacity);
    UFUNCTION(BlueprintCallable)
    void ClearDiscordParty();

  private:
    bool PresenceBusy = false;
    bool ClearPresenceRequested = false;
    FString PendingPresenceOperation;
    void DispatchPresenceChange(const FDiscordRichPresence &Presence, bool Clear);
    UFUNCTION()
    void OnPresenceCommand(FName Operation, bool bSuccess, const FBetideError &Error);
};

// DiscordPresenceGameInstance.cpp
#include "DiscordPresenceGameInstance.h"

DEFINE_LOG_CATEGORY_STATIC(LogGameDiscordPresence, Log, All);

void UDiscordPresenceGameInstance::PublishDiscordParty(const FString &PartyId,
                                                       const FString &JoinSecret, int32 Members,
                                                       int32 Capacity)
{
    if (PresenceBusy)
    {
        UE_LOG(LogGameDiscordPresence, Warning, TEXT("Wait for the current presence request"));
        return;
    }
    const UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord || !Discord->IsDiscordRunning() || !Discord->IsAuthenticated())
    {
        UE_LOG(LogGameDiscordPresence, Warning, TEXT("Sign in with Discord first"));
        return;
    }
    if (PartyId.Len() < 2 || PartyId.Len() > 128 || JoinSecret.Len() < 2 ||
        JoinSecret.Len() > 128 || Members < 1 || Capacity < Members)
    {
        UE_LOG(
            LogGameDiscordPresence, Warning,
            TEXT(
                "Use 2-128 characters for party ID and join secret; use 1 <= members <= capacity"));
        return;
    }
    FDiscordRichPresence Presence;
    Presence.Type = EDiscordActivityType::Playing;
    Presence.State = TEXT("Waiting for players");
    Presence.Details = TEXT("Co-op lobby");
    Presence.PartyId = PartyId;
    Presence.PartySize = Members;
    Presence.PartyMax = Capacity;
    Presence.JoinSecret = JoinSecret;
    Presence.SupportedPlatforms = static_cast<int32>(EDiscordActivityPlatform::Desktop);
    DispatchPresenceChange(Presence, false);
}

void UDiscordPresenceGameInstance::ClearDiscordParty()
{
    if (PresenceBusy)
    {
        ClearPresenceRequested = true;
        return;
    }
    ClearPresenceRequested = false;
    DispatchPresenceChange({}, true);
}

void UDiscordPresenceGameInstance::DispatchPresenceChange(const FDiscordRichPresence &Presence,
                                                          bool Clear)
{
    UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord)
    {
        UE_LOG(LogGameDiscordPresence, Warning, TEXT("Discord subsystem is unavailable"));
        return;
    }
    PendingPresenceOperation = Clear ? TEXT("ClearRichPresence") : TEXT("UpdateRichPresence");
    PresenceBusy = true;
    Discord->OnOperationComplete.AddUniqueDynamic(this, &ThisClass::OnPresenceCommand);
    if (Clear)
        Discord->ClearRichPresence();
    else
        Discord->UpdateRichPresence(Presence);
}

void UDiscordPresenceGameInstance::OnPresenceCommand(FName Operation, bool bSuccess,
                                                     const FBetideError &Error)
{
    if (!PresenceBusy || Operation.ToString() != PendingPresenceOperation)
        return;
    PresenceBusy = false;
    if (UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>())
        Discord->OnOperationComplete.RemoveDynamic(this, &ThisClass::OnPresenceCommand);
    if (bSuccess)
    {
        UE_LOG(LogGameDiscordPresence, Log, TEXT("%s"),
               PendingPresenceOperation == TEXT("UpdateRichPresence")
                   ? TEXT("Rich presence updated")
                   : TEXT("Clear request accepted by Discord SDK"));
    }
    else
        UE_LOG(LogGameDiscordPresence, Warning, TEXT("%s"), *Error.Message);
    if (ClearPresenceRequested)
        ClearDiscordParty();
}
```

Use the same **Party ID** for every member. For Discord lobbies, **Join Secret** is the secret used with **Create or Join Discord Lobby**. Keep it out of logs. Publish again when the party size or activity changes; Discord offers joins only while the party has room. Your game still controls admission.

This example advertises **Desktop**. For Android invites, configure the application's game invite deep link and include **Android** in **Supported Platforms**.

Keep one owner for presence changes. Call **Clear Discord Party** when the party ends; it waits for a pending publish before clearing. Update results come from the SDK callback. Clear success means the SDK accepted the call; the plugin does not receive a server acknowledgement for clearing.

## Send an invite or ask to join

Publish a joinable activity before inviting someone to your party. To request a place in another player's party, enable **Request to Join**; their activity must be for this game and have room.

**Blueprint**

<BlueprintViewer title="Send a Discord party invite" src="/blueprints/crossplay-discord-invite-sender.txt" />

Game Instance variables: `InviteSendBusy` (**Boolean**, false), `PendingInviteOperation` (**String**, empty).

**C++**

Game module dependencies: `DiscordGame`, `BetideCore`, `Engine`.

```cpp
// Copyright (c) 2026 Betide Studio. All Rights Reserved.
// DiscordInviteGameInstance.h
#pragma once
#include "Engine/GameInstance.h"
#include "DiscordGameSubsystem.h"
#include "DiscordInviteGameInstance.generated.h"

UCLASS()
class UDiscordInviteGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    UFUNCTION(BlueprintCallable)
    void SendDiscordPartyInvite(int64 UserId, const FString &Message, bool RequestToJoin);

  private:
    bool InviteSendBusy = false;
    FString PendingInviteOperation;
    UFUNCTION()
    void OnInviteCommand(FName Operation, bool bSuccess, const FBetideError &Error);
};

// DiscordInviteGameInstance.cpp
#include "DiscordInviteGameInstance.h"
DEFINE_LOG_CATEGORY_STATIC(LogGameDiscordInvite, Log, All);

void UDiscordInviteGameInstance::SendDiscordPartyInvite(int64 UserId, const FString &Message,
                                                        bool RequestToJoin)
{
    if (InviteSendBusy)
    {
        UE_LOG(LogGameDiscordInvite, Warning, TEXT("Wait for the current invite request"));
        return;
    }
    UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord || !Discord->IsDiscordRunning() || !Discord->IsAuthenticated())
    {
        UE_LOG(LogGameDiscordInvite, Warning, TEXT("Sign in with Discord first"));
        return;
    }
    if (UserId < 1)
    {
        UE_LOG(LogGameDiscordInvite, Warning, TEXT("Choose a valid Discord user ID"));
        return;
    }
    PendingInviteOperation =
        RequestToJoin ? TEXT("SendActivityJoinRequest") : TEXT("SendActivityInvite");
    InviteSendBusy = true;
    Discord->OnOperationComplete.AddUniqueDynamic(this, &ThisClass::OnInviteCommand);
    if (RequestToJoin)
        Discord->SendActivityJoinRequest(UserId);
    else
        Discord->SendActivityInvite(UserId, Message);
}

void UDiscordInviteGameInstance::OnInviteCommand(FName Operation, bool bSuccess,
                                                 const FBetideError &Error)
{
    if (!InviteSendBusy || Operation.ToString() != PendingInviteOperation)
        return;
    InviteSendBusy = false;
    if (UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>())
        Discord->OnOperationComplete.RemoveDynamic(this, &ThisClass::OnInviteCommand);
    if (bSuccess)
        UE_LOG(LogGameDiscordInvite, Log,
               TEXT("Invite request sent; waiting for the other player"));
    else
        UE_LOG(LogGameDiscordInvite, Warning, TEXT("%s"), *Error.Message);
}
```

Use one owner for these sends: completion identifies the operation, not the recipient. Success confirms the send, not that anyone joined. **Message** is optional and applies only to direct invites.

Discord allows an invite when both players are online in the game and have not blocked each other, are Discord friends, or share a Discord server and have previously exchanged DMs.

## Accept an invite and join its lobby

Pass a received **Join** invite's **Message ID** to **Accept Discord Party Invite**. The plugin rejects missing, expired, or wrong-type invites. The sender must use its Discord lobby secret as the activity's join secret.

**Blueprint**

<BlueprintViewer title="Accept a Discord invite and join" src="/blueprints/crossplay-discord-invite-join.txt" />

Game Instance variables: `PendingInviteId`, `JoinedLobbyId` (**Integer64**, 0), `JoiningDiscordLobby`, `LeavingDiscordLobby` (**Boolean**, false), `DiscordLobbyMembers` (**Discord Lobby Member Summary** array, empty).

**C++**

Game module dependencies: `DiscordGame`, `BetideCore`, `Engine`.

```cpp
// Copyright (c) 2026 Betide Studio. All Rights Reserved.
// DiscordInviteJoinGameInstance.h
#pragma once
#include "Engine/GameInstance.h"
#include "DiscordGameSubsystem.h"
#include "DiscordInviteJoinGameInstance.generated.h"

UCLASS()
class UDiscordInviteJoinGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    UFUNCTION(BlueprintCallable)
    void AcceptDiscordPartyInvite(int64 InviteMessageId);
    UFUNCTION(BlueprintCallable)
    void JoinDiscordParty(const FString &SharedSecret);
    UFUNCTION(BlueprintCallable)
    void LeaveDiscordParty();
    UPROPERTY(BlueprintReadOnly)
    int64 JoinedLobbyId = 0;
    UPROPERTY(BlueprintReadOnly)
    TArray<FDiscordLobbyMemberSummary> DiscordLobbyMembers;

  private:
    int64 PendingInviteId = 0;
    UFUNCTION()
    void OnInviteAccepted(int64 InviteMessageId, FString JoinSecret, const FBetideError &Error);
    bool JoiningDiscordLobby = false;
    bool LeavingDiscordLobby = false;
    UFUNCTION()
    void OnLobbyId(FName Operation, bool bSuccess, int64 ResultId, const FBetideError &Error);
    UFUNCTION()
    void OnLobbyCommand(FName Operation, bool bSuccess, const FBetideError &Error);
    UFUNCTION()
    void OnLobbyUpdate(const TArray<FDiscordLobbySummary> &Lobbies);
    void RefreshDiscordMembers();
};

// DiscordInviteJoinGameInstance.cpp
#include "DiscordInviteJoinGameInstance.h"

DEFINE_LOG_CATEGORY_STATIC(LogGameDiscordInviteJoin, Log, All);

void UDiscordInviteJoinGameInstance::JoinDiscordParty(const FString &SharedSecret)
{
    if (PendingInviteId > 0 || JoiningDiscordLobby || LeavingDiscordLobby || JoinedLobbyId > 0)
    {
        UE_LOG(LogGameDiscordInviteJoin, Warning,
               TEXT("Already joined or a lobby request is pending"));
        return;
    }
    UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord || !Discord->IsDiscordRunning() || !Discord->IsAuthenticated())
    {
        UE_LOG(LogGameDiscordInviteJoin, Warning, TEXT("Sign in with Discord first"));
        return;
    }
    JoiningDiscordLobby = true;
    Discord->OnIdOperationComplete.AddUniqueDynamic(this, &ThisClass::OnLobbyId);
    Discord->OnLobbiesUpdated.AddUniqueDynamic(this, &ThisClass::OnLobbyUpdate);
    TMap<FString, FString> Metadata;
    Metadata.Add(TEXT("mode"), TEXT("coop"));
    Discord->CreateOrJoinDiscordLobby(SharedSecret, Metadata, {});
}

void UDiscordInviteJoinGameInstance::OnLobbyId(FName Operation, bool bSuccess, int64 ResultId,
                                               const FBetideError &Error)
{
    if (Operation != TEXT("CreateOrJoinLobby") || !JoiningDiscordLobby)
        return;
    JoiningDiscordLobby = false;
    UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord)
        return;
    Discord->OnIdOperationComplete.RemoveDynamic(this, &ThisClass::OnLobbyId);
    if (!bSuccess)
    {
        Discord->OnLobbiesUpdated.RemoveDynamic(this, &ThisClass::OnLobbyUpdate);
        UE_LOG(LogGameDiscordInviteJoin, Warning, TEXT("%s"), *Error.Message);
        return;
    }
    JoinedLobbyId = ResultId;
    RefreshDiscordMembers();
}

void UDiscordInviteJoinGameInstance::OnLobbyUpdate(const TArray<FDiscordLobbySummary> &)
{
    RefreshDiscordMembers();
}

void UDiscordInviteJoinGameInstance::RefreshDiscordMembers()
{
    if (JoinedLobbyId <= 0)
        return;
    const UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord)
        return;
    DiscordLobbyMembers = Discord->GetDiscordLobbyMembers(JoinedLobbyId);
    for (const FDiscordLobbyMemberSummary &Member : DiscordLobbyMembers)
    {
        UE_LOG(LogGameDiscordInviteJoin, Log, TEXT("%s (%lld), connected: %s"), *Member.DisplayName,
               Member.UserId, Member.bConnected ? TEXT("true") : TEXT("false"));
    }
}

void UDiscordInviteJoinGameInstance::LeaveDiscordParty()
{
    if (PendingInviteId > 0 || JoiningDiscordLobby || LeavingDiscordLobby)
    {
        UE_LOG(LogGameDiscordInviteJoin, Warning, TEXT("A lobby request is pending"));
        return;
    }
    if (JoinedLobbyId <= 0)
    {
        UE_LOG(LogGameDiscordInviteJoin, Warning, TEXT("No joined Discord lobby"));
        return;
    }
    UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord)
        return;
    LeavingDiscordLobby = true;
    Discord->OnOperationComplete.AddUniqueDynamic(this, &ThisClass::OnLobbyCommand);
    Discord->LeaveDiscordLobby(JoinedLobbyId);
}

void UDiscordInviteJoinGameInstance::OnLobbyCommand(FName Operation, bool bSuccess,
                                                    const FBetideError &Error)
{
    if (Operation != TEXT("LeaveLobby") || !LeavingDiscordLobby)
        return;
    LeavingDiscordLobby = false;
    UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord)
        return;
    Discord->OnOperationComplete.RemoveDynamic(this, &ThisClass::OnLobbyCommand);
    if (!bSuccess)
    {
        UE_LOG(LogGameDiscordInviteJoin, Warning, TEXT("%s"), *Error.Message);
        return;
    }
    JoinedLobbyId = 0;
    DiscordLobbyMembers.Reset();
    Discord->OnLobbiesUpdated.RemoveDynamic(this, &ThisClass::OnLobbyUpdate);
    UE_LOG(LogGameDiscordInviteJoin, Log, TEXT("Left Discord lobby"));
}

void UDiscordInviteJoinGameInstance::AcceptDiscordPartyInvite(int64 InviteMessageId)
{
    if (PendingInviteId > 0 || JoiningDiscordLobby || LeavingDiscordLobby || JoinedLobbyId > 0)
    {
        UE_LOG(LogGameDiscordInviteJoin, Warning,
               TEXT("Leave the current party and wait for pending requests first"));
        return;
    }
    UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord || !Discord->IsDiscordRunning() || !Discord->IsAuthenticated())
    {
        UE_LOG(LogGameDiscordInviteJoin, Warning, TEXT("Sign in with Discord first"));
        return;
    }
    if (InviteMessageId <= 0)
    {
        UE_LOG(LogGameDiscordInviteJoin, Warning, TEXT("Choose a received Join invite"));
        return;
    }
    PendingInviteId = InviteMessageId;
    Discord->OnActivityInviteAccepted.AddUniqueDynamic(this, &ThisClass::OnInviteAccepted);
    Discord->AcceptDiscordActivityInvite(InviteMessageId);
}

void UDiscordInviteJoinGameInstance::OnInviteAccepted(int64 InviteMessageId, FString JoinSecret,
                                                      const FBetideError &Error)
{
    if (PendingInviteId <= 0 || InviteMessageId != PendingInviteId)
        return;
    PendingInviteId = 0;
    if (UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>())
        Discord->OnActivityInviteAccepted.RemoveDynamic(this, &ThisClass::OnInviteAccepted);
    if (Error.IsError())
    {
        UE_LOG(LogGameDiscordInviteJoin, Warning, TEXT("%s"), *Error.Message);
        return;
    }
    JoinDiscordParty(JoinSecret);
}
```

This joins the Discord lobby and refreshes its member list; it does not connect an Unreal game session. Keep one owner for lobby requests, and leave the current party before accepting another invite. A failed leave keeps **Joined Lobby ID** for retry. Stop any voice call before leaving.

The join secret stays inside the flow. Do not print it or treat accepting the invite as a successful lobby join.

## Receive invites and approve requests

**Received Invites** holds this running client's invite list. A **Join** row is an invitation to accept; a **Join Request** row asks you to send an invitation back. Only approve requests while your party has room and its joinable activity is current.

**Blueprint**

<BlueprintViewer title="Discord invite inbox and approvals" src="/blueprints/crossplay-discord-invite-inbox.txt" />

Game Instance variables: `InviteInboxOpen`, `InviteReplyBusy`, `InviteInboxCloseRequested` (**Boolean**, false), `ReceivedInvites` (**Discord Activity Invite Summary** array, empty).

**C++**

Game module dependencies: `DiscordGame`, `BetideCore`, `Engine`.

```cpp
// Copyright (c) 2026 Betide Studio. All Rights Reserved.
// DiscordInviteInboxGameInstance.h
#pragma once
#include "Engine/GameInstance.h"
#include "DiscordGameSubsystem.h"
#include "DiscordInviteInboxGameInstance.generated.h"

UCLASS()
class UDiscordInviteInboxGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    UFUNCTION(BlueprintCallable)
    void OpenDiscordInviteInbox();
    UFUNCTION(BlueprintCallable)
    void ApproveDiscordJoinRequest(const FDiscordActivityInviteSummary &Invite);
    UFUNCTION(BlueprintCallable)
    void CloseDiscordInviteInbox();
    UPROPERTY(BlueprintReadOnly)
    TArray<FDiscordActivityInviteSummary> ReceivedInvites;

  private:
    bool InviteInboxOpen = false;
    bool InviteReplyBusy = false;
    bool InviteInboxCloseRequested = false;
    void RefreshDiscordInviteInbox();
    UFUNCTION()
    void OnInviteUpdates(const TArray<FDiscordActivityInviteSummary> &Invites);
    UFUNCTION()
    void OnInviteReply(FName Operation, bool bSuccess, const FBetideError &Error);
};

// DiscordInviteInboxGameInstance.cpp
#include "DiscordInviteInboxGameInstance.h"
DEFINE_LOG_CATEGORY_STATIC(LogGameDiscordInbox, Log, All);

void UDiscordInviteInboxGameInstance::OpenDiscordInviteInbox()
{
    if (InviteInboxOpen)
        return;
    UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord || !Discord->IsDiscordRunning() || !Discord->IsAuthenticated())
    {
        UE_LOG(LogGameDiscordInbox, Warning, TEXT("Sign in with Discord first"));
        return;
    }
    InviteInboxOpen = true;
    Discord->OnActivityInvitesUpdated.AddUniqueDynamic(this, &ThisClass::OnInviteUpdates);
    RefreshDiscordInviteInbox();
}

void UDiscordInviteInboxGameInstance::RefreshDiscordInviteInbox()
{
    if (!InviteInboxOpen)
        return;
    const UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord)
        return;
    ReceivedInvites = Discord->GetDiscordActivityInvites();
    UE_LOG(LogGameDiscordInbox, Log, TEXT("%d received activity invites"), ReceivedInvites.Num());
}

void UDiscordInviteInboxGameInstance::OnInviteUpdates(const TArray<FDiscordActivityInviteSummary> &)
{
    RefreshDiscordInviteInbox();
}

void UDiscordInviteInboxGameInstance::ApproveDiscordJoinRequest(
    const FDiscordActivityInviteSummary &Invite)
{
    if (!InviteInboxOpen)
    {
        UE_LOG(LogGameDiscordInbox, Warning, TEXT("Open the invite inbox first"));
        return;
    }
    if (InviteReplyBusy || InviteInboxCloseRequested)
    {
        UE_LOG(LogGameDiscordInbox, Warning, TEXT("Wait for the pending reply or close"));
        return;
    }
    UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord || !Discord->IsDiscordRunning() || !Discord->IsAuthenticated())
    {
        UE_LOG(LogGameDiscordInbox, Warning, TEXT("Sign in with Discord first"));
        return;
    }
    if (!Invite.bIsValid || Invite.Type != EDiscordActivityInviteType::JoinRequest)
    {
        UE_LOG(LogGameDiscordInbox, Warning, TEXT("Choose a valid Join Request invite"));
        return;
    }
    InviteReplyBusy = true;
    Discord->OnOperationComplete.AddUniqueDynamic(this, &ThisClass::OnInviteReply);
    Discord->ReplyToDiscordActivityJoinRequest(Invite.MessageId);
}

void UDiscordInviteInboxGameInstance::OnInviteReply(FName Operation, bool bSuccess,
                                                    const FBetideError &Error)
{
    if (!InviteReplyBusy || Operation != TEXT("ReplyToActivityJoinRequest"))
        return;
    InviteReplyBusy = false;
    if (UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>())
        Discord->OnOperationComplete.RemoveDynamic(this, &ThisClass::OnInviteReply);
    if (bSuccess)
        UE_LOG(LogGameDiscordInbox, Log,
               TEXT("Approval sent; the requester must accept the Join invite"));
    else
        UE_LOG(LogGameDiscordInbox, Warning, TEXT("%s"), *Error.Message);
    if (InviteInboxCloseRequested)
        CloseDiscordInviteInbox();
    else
        RefreshDiscordInviteInbox();
}

void UDiscordInviteInboxGameInstance::CloseDiscordInviteInbox()
{
    if (InviteReplyBusy)
    {
        InviteInboxCloseRequested = true;
        return;
    }
    InviteInboxCloseRequested = false;
    InviteInboxOpen = false;
    if (UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>())
        Discord->OnActivityInvitesUpdated.RemoveDynamic(this, &ThisClass::OnInviteUpdates);
    ReceivedInvites.Reset();
}
```

Use **Is Valid** to disable expired rows. The list is a local cache, not invite history; invalid entries disappear on a later update. The plugin checks validity again when you approve or accept.

Keep one owner for approval requests because their completion does not include an invite ID. **Close Discord Invite Inbox** waits for an active reply, then removes its listeners and clears the list. Close before signing out.

## Join from a launch or resume invite

Call **Enable Discord Invite Joins** after login reaches **Ready**. Before that, leave **On Discord Activity Join Requested** unbound; the plugin retains the latest secret through the login screen.

**Blueprint**

<BlueprintViewer title="Join from a Discord launch or resume invite" src="/blueprints/crossplay-discord-launch-join.txt" />

Game Instance variables: `ListeningForInvites`, `JoiningDiscordLobby`, `LeavingDiscordLobby` (**Boolean**, false), `PendingJoinSecret`, `ActiveJoinSecret` (**String**, empty), `JoinedLobbyId` (**Integer64**, 0), `DiscordLobbyMembers` (**Discord Lobby Member Summary** array, empty).

**C++**

Game module dependencies: `DiscordGame`, `BetideCore`, `Engine`.

```cpp
// Copyright (c) 2026 Betide Studio. All Rights Reserved.
// DiscordLaunchJoinGameInstance.h
#pragma once
#include "Engine/GameInstance.h"
#include "DiscordGameSubsystem.h"
#include "DiscordLaunchJoinGameInstance.generated.h"

UCLASS()
class UDiscordLaunchJoinGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    UFUNCTION(BlueprintCallable)
    void JoinPendingDiscordInvite();
    UFUNCTION(BlueprintCallable)
    void EnableDiscordInviteJoins();
    UFUNCTION(BlueprintCallable)
    void DisableDiscordInviteJoins();
    UFUNCTION(BlueprintCallable)
    void DiscardDiscordJoinInvite();
    UFUNCTION(BlueprintCallable)
    void LeaveDiscordParty();
    UPROPERTY(BlueprintReadOnly)
    int64 JoinedLobbyId = 0;
    UPROPERTY(BlueprintReadOnly)
    TArray<FDiscordLobbyMemberSummary> DiscordLobbyMembers;

  private:
    bool ListeningForInvites = false;
    FString PendingJoinSecret;
    FString ActiveJoinSecret;
    void QueueDiscordJoinSecret(const FString &JoinSecret);
    UFUNCTION()
    void OnActivityJoin(int64 ApplicationId, FString JoinSecret);
    bool JoiningDiscordLobby = false;
    bool LeavingDiscordLobby = false;
    UFUNCTION()
    void OnLobbyId(FName Operation, bool bSuccess, int64 ResultId, const FBetideError &Error);
    UFUNCTION()
    void OnLobbyCommand(FName Operation, bool bSuccess, const FBetideError &Error);
    UFUNCTION()
    void OnLobbyUpdate(const TArray<FDiscordLobbySummary> &Lobbies);
    void RefreshDiscordMembers();
};

// DiscordLaunchJoinGameInstance.cpp
#include "DiscordLaunchJoinGameInstance.h"

DEFINE_LOG_CATEGORY_STATIC(LogGameDiscordLaunch, Log, All);

void UDiscordLaunchJoinGameInstance::JoinPendingDiscordInvite()
{
    if (JoiningDiscordLobby || LeavingDiscordLobby || JoinedLobbyId > 0)
    {
        UE_LOG(LogGameDiscordLaunch, Warning, TEXT("Already joined or a lobby request is pending"));
        return;
    }
    UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord || !Discord->IsDiscordRunning() || !Discord->IsAuthenticated())
    {
        UE_LOG(LogGameDiscordLaunch, Warning, TEXT("Sign in with Discord first"));
        return;
    }
    ActiveJoinSecret = PendingJoinSecret;
    JoiningDiscordLobby = true;
    Discord->OnIdOperationComplete.AddUniqueDynamic(this, &ThisClass::OnLobbyId);
    Discord->OnLobbiesUpdated.AddUniqueDynamic(this, &ThisClass::OnLobbyUpdate);
    TMap<FString, FString> Metadata;
    Metadata.Add(TEXT("mode"), TEXT("coop"));
    Discord->CreateOrJoinDiscordLobby(ActiveJoinSecret, Metadata, {});
}

void UDiscordLaunchJoinGameInstance::OnLobbyId(FName Operation, bool bSuccess, int64 ResultId,
                                               const FBetideError &Error)
{
    if (Operation != TEXT("CreateOrJoinLobby") || !JoiningDiscordLobby)
        return;
    JoiningDiscordLobby = false;
    UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord)
        return;
    Discord->OnIdOperationComplete.RemoveDynamic(this, &ThisClass::OnLobbyId);
    if (!bSuccess)
    {
        Discord->OnLobbiesUpdated.RemoveDynamic(this, &ThisClass::OnLobbyUpdate);
        ActiveJoinSecret.Reset();
        UE_LOG(LogGameDiscordLaunch, Warning, TEXT("%s"), *Error.Message);
        return;
    }
    JoinedLobbyId = ResultId;
    if (PendingJoinSecret == ActiveJoinSecret)
        PendingJoinSecret.Reset();
    RefreshDiscordMembers();
    ActiveJoinSecret.Reset();
}

void UDiscordLaunchJoinGameInstance::OnLobbyUpdate(const TArray<FDiscordLobbySummary> &)
{
    RefreshDiscordMembers();
}

void UDiscordLaunchJoinGameInstance::RefreshDiscordMembers()
{
    if (JoinedLobbyId <= 0)
        return;
    const UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord)
        return;
    DiscordLobbyMembers = Discord->GetDiscordLobbyMembers(JoinedLobbyId);
    for (const FDiscordLobbyMemberSummary &Member : DiscordLobbyMembers)
    {
        UE_LOG(LogGameDiscordLaunch, Log, TEXT("%s (%lld), connected: %s"), *Member.DisplayName,
               Member.UserId, Member.bConnected ? TEXT("true") : TEXT("false"));
    }
}

void UDiscordLaunchJoinGameInstance::LeaveDiscordParty()
{
    if (JoiningDiscordLobby || LeavingDiscordLobby)
    {
        UE_LOG(LogGameDiscordLaunch, Warning, TEXT("A lobby request is pending"));
        return;
    }
    if (JoinedLobbyId <= 0)
    {
        UE_LOG(LogGameDiscordLaunch, Warning, TEXT("No joined Discord lobby"));
        return;
    }
    UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord)
        return;
    LeavingDiscordLobby = true;
    Discord->OnOperationComplete.AddUniqueDynamic(this, &ThisClass::OnLobbyCommand);
    Discord->LeaveDiscordLobby(JoinedLobbyId);
}

void UDiscordLaunchJoinGameInstance::OnLobbyCommand(FName Operation, bool bSuccess,
                                                    const FBetideError &Error)
{
    if (Operation != TEXT("LeaveLobby") || !LeavingDiscordLobby)
        return;
    LeavingDiscordLobby = false;
    UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord)
        return;
    Discord->OnOperationComplete.RemoveDynamic(this, &ThisClass::OnLobbyCommand);
    if (!bSuccess)
    {
        UE_LOG(LogGameDiscordLaunch, Warning, TEXT("%s"), *Error.Message);
        return;
    }
    JoinedLobbyId = 0;
    DiscordLobbyMembers.Reset();
    Discord->OnLobbiesUpdated.RemoveDynamic(this, &ThisClass::OnLobbyUpdate);
    UE_LOG(LogGameDiscordLaunch, Log, TEXT("Left Discord lobby"));
}

void UDiscordLaunchJoinGameInstance::EnableDiscordInviteJoins()
{
    if (ListeningForInvites)
        return;
    UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord || !Discord->IsDiscordRunning() || !Discord->IsAuthenticated())
    {
        UE_LOG(LogGameDiscordLaunch, Warning, TEXT("Sign in with Discord first"));
        return;
    }
    ListeningForInvites = true;
    Discord->OnActivityJoinRequested.AddUniqueDynamic(this, &ThisClass::OnActivityJoin);
    int64 ApplicationId = 0;
    FString JoinSecret;
    if (Discord->ConsumePendingDiscordActivityJoin(ApplicationId, JoinSecret))
        QueueDiscordJoinSecret(JoinSecret);
}

void UDiscordLaunchJoinGameInstance::OnActivityJoin(int64 ApplicationId, FString JoinSecret)
{
    QueueDiscordJoinSecret(JoinSecret);
}

void UDiscordLaunchJoinGameInstance::QueueDiscordJoinSecret(const FString &JoinSecret)
{
    if (JoinSecret.IsEmpty())
        return;
    PendingJoinSecret = JoinSecret;
    UE_LOG(LogGameDiscordLaunch, Log, TEXT("A Discord party invite is ready to join"));
}

void UDiscordLaunchJoinGameInstance::DisableDiscordInviteJoins()
{
    ListeningForInvites = false;
    if (UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>())
        Discord->OnActivityJoinRequested.RemoveDynamic(this, &ThisClass::OnActivityJoin);
}

void UDiscordLaunchJoinGameInstance::DiscardDiscordJoinInvite()
{
    PendingJoinSecret.Reset();
    if (UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>())
        Discord->ClearPendingDiscordActivityJoin();
}
```

Let the player choose **Join Pending Discord Invite** or **Discard Discord Join Invite**. Leave the current party first. A failed join keeps the secret for retry; a successful join clears only the secret it used, preserving a newer invite. Neither secret is printed or saved to disk.

Before logout, finish pending lobby work, leave the lobby, then disable invite joins and discard the stored invite. Disabling removes the listener; discarding does not cancel an in-flight join. Keep one Game Instance responsible for this flow—the plugin's pending slot is shared within the process.

For desktop launch, register **Register Discord Launch Command** or **Register Discord Launch Steam Application** during setup and check the returned Boolean. For Android, configure the application's game invite deep link and include Android in the activity's supported platforms. Launch arguments accept `-DiscordJoinSecret=` or the `/_discord/join?secret=...` URL; do not put real secrets in logs.

## Friends

Open the list after login. **Relationships** contains both Discord-wide and game relationships; each row has separate **Discord Relationship** and **Game Relationship** values. **Grouped Relationships** contains the selected **Group**: online in this game, online elsewhere, or offline.

**Blueprint**

<BlueprintViewer title="Discord friend list and username requests" src="/blueprints/crossplay-discord-friends.txt" />

Game Instance variables: `FriendsOpen`, `FriendRequestBusy`, `FriendsCloseRequested` (**Boolean**, false), `PendingFriendOperation` (**String**, empty), `RelationshipGroup` (**Discord Relationship Group**, Online Playing Game), `Relationships`, `GroupedRelationships` (**Discord Relationship Summary** arrays, empty).

**C++**

Game module dependencies: `DiscordGame`, `BetideCore`, `Engine`.

```cpp
// Copyright (c) 2026 Betide Studio. All Rights Reserved.
// DiscordFriendsGameInstance.h
#pragma once
#include "Engine/GameInstance.h"
#include "DiscordGameSubsystem.h"
#include "DiscordFriendsGameInstance.generated.h"

UCLASS()
class UDiscordFriendsGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    UFUNCTION(BlueprintCallable)
    void OpenDiscordFriends(EDiscordRelationshipGroup Group);
    UFUNCTION(BlueprintCallable)
    void SendDiscordFriendRequest(const FString &Username, bool GameFriend);
    UFUNCTION(BlueprintCallable)
    void CloseDiscordFriends();
    UPROPERTY(BlueprintReadOnly)
    TArray<FDiscordRelationshipSummary> Relationships;
    UPROPERTY(BlueprintReadOnly)
    TArray<FDiscordRelationshipSummary> GroupedRelationships;

  private:
    FString PendingFriendOperation;
    EDiscordRelationshipGroup RelationshipGroup = EDiscordRelationshipGroup::OnlinePlayingGame;
    bool FriendsOpen = false;
    bool FriendRequestBusy = false;
    bool FriendsCloseRequested = false;
    void RefreshDiscordFriends();
    UFUNCTION()
    void OnRelationshipUpdates(const TArray<FDiscordRelationshipSummary> &UpdatedRelationships);
    UFUNCTION()
    void OnFriendRequest(FName Operation, bool bSuccess, const FBetideError &Error);
};

// DiscordFriendsGameInstance.cpp
#include "DiscordFriendsGameInstance.h"
DEFINE_LOG_CATEGORY_STATIC(LogGameDiscordFriends, Log, All);

void UDiscordFriendsGameInstance::OpenDiscordFriends(EDiscordRelationshipGroup Group)
{
    if (FriendsOpen)
        return;
    UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord || !Discord->IsDiscordRunning() || !Discord->IsAuthenticated())
    {
        UE_LOG(LogGameDiscordFriends, Warning, TEXT("Sign in with Discord first"));
        return;
    }
    RelationshipGroup = Group;
    FriendsOpen = true;
    Discord->OnRelationshipsUpdated.AddUniqueDynamic(this, &ThisClass::OnRelationshipUpdates);
    RefreshDiscordFriends();
}

void UDiscordFriendsGameInstance::RefreshDiscordFriends()
{
    if (!FriendsOpen)
        return;
    const UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord)
        return;
    Relationships = Discord->GetRelationships();
    GroupedRelationships = Discord->GetRelationshipsByGroup(RelationshipGroup);
    UE_LOG(LogGameDiscordFriends, Log, TEXT("%d relationships"), Relationships.Num());
}

void UDiscordFriendsGameInstance::OnRelationshipUpdates(const TArray<FDiscordRelationshipSummary> &)
{
    RefreshDiscordFriends();
}

void UDiscordFriendsGameInstance::SendDiscordFriendRequest(const FString &Username, bool GameFriend)
{
    if (!FriendsOpen)
    {
        UE_LOG(LogGameDiscordFriends, Warning, TEXT("Open the friends list first"));
        return;
    }
    if (FriendRequestBusy || FriendsCloseRequested)
    {
        UE_LOG(LogGameDiscordFriends, Warning,
               TEXT("Wait for the pending friend request or close"));
        return;
    }
    UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord || !Discord->IsDiscordRunning() || !Discord->IsAuthenticated())
    {
        UE_LOG(LogGameDiscordFriends, Warning, TEXT("Sign in with Discord first"));
        return;
    }
    PendingFriendOperation =
        GameFriend ? TEXT("SendGameFriendRequest") : TEXT("SendDiscordFriendRequest");
    FriendRequestBusy = true;
    Discord->OnOperationComplete.AddUniqueDynamic(this, &ThisClass::OnFriendRequest);
    if (GameFriend)
        Discord->SendGameFriendRequestByUsername(Username);
    else
        Discord->SendDiscordFriendRequestByUsername(Username);
}

void UDiscordFriendsGameInstance::OnFriendRequest(FName Operation, bool bSuccess,
                                                  const FBetideError &Error)
{
    if (!FriendRequestBusy || Operation.ToString() != PendingFriendOperation)
        return;
    FriendRequestBusy = false;
    if (UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>())
        Discord->OnOperationComplete.RemoveDynamic(this, &ThisClass::OnFriendRequest);
    if (bSuccess)
        UE_LOG(LogGameDiscordFriends, Log,
               TEXT("Friend request completed; check the relationship status"));
    else
        UE_LOG(LogGameDiscordFriends, Warning, TEXT("%s"), *Error.Message);
    if (FriendsCloseRequested)
        CloseDiscordFriends();
    else
        RefreshDiscordFriends();
}

void UDiscordFriendsGameInstance::CloseDiscordFriends()
{
    if (FriendRequestBusy)
    {
        FriendsCloseRequested = true;
        return;
    }
    FriendsCloseRequested = false;
    FriendsOpen = false;
    if (UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>())
        Discord->OnRelationshipsUpdated.RemoveDynamic(this, &ThisClass::OnRelationshipUpdates);
    Relationships.Reset();
    GroupedRelationships.Reset();
}
```

Use the exact unique username. Enable **Game Friend** for a relationship in this game; otherwise the request is Discord-wide. Sending normally creates an outgoing request. If that player already sent you a request of the same kind, sending accepts it instead. The list refreshes when Discord reports relationship changes.

Keep one owner for friend requests; completions do not identify the recipient. **Close Discord Friends** waits for an active request, removes listeners, and clears both lists. Close before logout or reopening with another group. Use **Spam Request** to separate suspicious incoming requests from the regular inbox.

## Change a relationship

Use the selected relationship's **User ID**. Match the action to its Discord or game relationship: **Accept**/**Reject** for incoming requests, **Cancel** for outgoing requests, and **Remove** for existing friends.

**Blueprint**

<BlueprintViewer title="Change a Discord relationship" src="/blueprints/crossplay-discord-relationship-action.txt" />

Game Instance variables: `RelationshipBusy` (**Boolean**, false), `PendingRelationshipOperation` (**String**, empty), `PendingRelationshipUserId` (**Integer64**, 0).

**C++**

Game module dependencies: `DiscordGame`, `BetideCore`, `Engine`.

```cpp
// Copyright (c) 2026 Betide Studio. All Rights Reserved.
// DiscordRelationshipGameInstance.h
#pragma once
#include "Engine/GameInstance.h"
#include "DiscordGameSubsystem.h"
#include "DiscordRelationshipGameInstance.generated.h"

UCLASS()
class UDiscordRelationshipGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    UFUNCTION(BlueprintCallable)
    void ChangeDiscordRelationship(int64 UserId, EDiscordFriendAction Action);

  private:
    bool RelationshipBusy = false;
    FString PendingRelationshipOperation;
    int64 PendingRelationshipUserId = 0;
    UFUNCTION()
    void OnRelationshipCommand(FName Operation, bool bSuccess, const FBetideError &Error);
};

// DiscordRelationshipGameInstance.cpp
#include "DiscordRelationshipGameInstance.h"
DEFINE_LOG_CATEGORY_STATIC(LogGameDiscordRelationship, Log, All);

void UDiscordRelationshipGameInstance::ChangeDiscordRelationship(int64 UserId,
                                                                 EDiscordFriendAction Action)
{
    if (RelationshipBusy)
    {
        UE_LOG(LogGameDiscordRelationship, Warning,
               TEXT("Wait for the current relationship request"));
        return;
    }
    UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>();
    if (!Discord || !Discord->IsDiscordRunning() || !Discord->IsAuthenticated())
    {
        UE_LOG(LogGameDiscordRelationship, Warning, TEXT("Sign in with Discord first"));
        return;
    }
    if (UserId <= 0)
    {
        UE_LOG(LogGameDiscordRelationship, Warning, TEXT("Choose a valid Discord user ID"));
        return;
    }
    static const TMap<EDiscordFriendAction, FString> Operations = {
        {EDiscordFriendAction::SendDiscordRequest, TEXT("SendDiscordFriendRequest")},
        {EDiscordFriendAction::SendGameRequest, TEXT("SendGameFriendRequest")},
        {EDiscordFriendAction::AcceptDiscordRequest, TEXT("AcceptDiscordFriendRequest")},
        {EDiscordFriendAction::AcceptGameRequest, TEXT("AcceptGameFriendRequest")},
        {EDiscordFriendAction::RejectDiscordRequest, TEXT("RejectDiscordFriendRequest")},
        {EDiscordFriendAction::RejectGameRequest, TEXT("RejectGameFriendRequest")},
        {EDiscordFriendAction::RemoveFriend, TEXT("RemoveDiscordAndGameFriend")},
        {EDiscordFriendAction::Block, TEXT("BlockUser")},
        {EDiscordFriendAction::Unblock, TEXT("UnblockUser")},
        {EDiscordFriendAction::CancelDiscordRequest, TEXT("CancelDiscordFriendRequest")},
        {EDiscordFriendAction::CancelGameRequest, TEXT("CancelGameFriendRequest")},
        {EDiscordFriendAction::RemoveGameFriend, TEXT("RemoveGameFriend")}};
    const FString *ExpectedOperation = Operations.Find(Action);
    if (!ExpectedOperation)
    {
        UE_LOG(LogGameDiscordRelationship, Warning, TEXT("Choose a supported relationship action"));
        return;
    }
    PendingRelationshipOperation = *ExpectedOperation;
    PendingRelationshipUserId = UserId;
    RelationshipBusy = true;
    Discord->OnOperationComplete.AddUniqueDynamic(this, &ThisClass::OnRelationshipCommand);
    Discord->UpdateFriendship(UserId, Action);
}

void UDiscordRelationshipGameInstance::OnRelationshipCommand(FName Operation, bool bSuccess,
                                                             const FBetideError &Error)
{
    if (!RelationshipBusy || (Operation.ToString() != PendingRelationshipOperation &&
                              Operation != TEXT("UpdateFriendship")))
        return;
    RelationshipBusy = false;
    if (UDiscordGameSubsystem *Discord = GetSubsystem<UDiscordGameSubsystem>())
        Discord->OnOperationComplete.RemoveDynamic(this, &ThisClass::OnRelationshipCommand);
    if (bSuccess)
        UE_LOG(LogGameDiscordRelationship, Log, TEXT("%s completed for %lld"),
               *PendingRelationshipOperation, PendingRelationshipUserId);
    else
        UE_LOG(LogGameDiscordRelationship, Warning, TEXT("%s for %lld: %s"),
               *PendingRelationshipOperation, PendingRelationshipUserId, *Error.Message);
}
```

**Remove Friend** removes both Discord and game friendship; **Remove Game Friend** removes only the game relationship. **Block** applies across Discord and games and removes existing relationships. **Unblock** does not restore them.

Route relationship actions and username requests through one owner and wait for completion before sending another. Completion identifies the action, not the player; immediate validation failures use `UpdateFriendship`. Refresh the list on **On Relationships Updated** rather than assuming the cache changes when the command finishes.

<Accordion>
  <AccordionItem title="Requests fail despite allowing friends">

The recipient must have authorized the same application. Discord can report HTTP 400/code `80000` for a Discord-wide request when that authorization is absent, even if incoming requests are enabled. Code `80009` indicates missing shared authorization for a game-friend request.

Check the authenticated sender, exact recipient ID, relationship kind, and full response error. A display name is not necessarily a unique username. Keep expired activity invites visible only long enough to explain why they cannot be accepted.

  </AccordionItem>
</Accordion>
