---
title: "Matchmaking with Edgegap"
description: "Match players and receive the connection details for their server."
seo:
  noindex: true
---

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

First [publish and test your server](/integrations/edgegap/deployment). Configure an Edgegap matchmaker profile with the player count, matching rules, and application version it should deploy.

## Setup

In **Betide → Crossplay → Edgegap**, set **Matchmaker URL** and its **Auth Token**. Their environment overrides are `EDGEGAP_MM_URL` and `EDGEGAP_MATCHMAKER_KEY`.

Use the matchmaker's client token, not the main Edgegap API key. The client token is stored in project configuration and is intended for distribution with the game. [Edgegap token and group model](https://docs.edgegap.com/learn/matchmaking/matchmaker-in-depth)

Select Edgegap in the **Matchmaking** policy. The queue name must exactly match a remote **profile**; local player-count and skill settings do not rewrite that profile.

## Queue one player

**Unified queue-to-travel example blocked:** matchmaking replaces the host with a complete `host:port` URL while retaining its port. Travel then appends that port again. The shared matchmaking result needs fixing before this flow is usable.

<BlueprintViewer title="Find an Edgegap match" nodeNames={["Start Matchmaking", "Cancel Matchmaking", "Travel To Crossplay Connect Info"]} />

The adapter creates a ready group for one player. `MATCH_FOUND` waits for allocation; `HOST_ASSIGNED` provides the destination, with an external port named `gameport`. Do not request a second server. [Edgegap assignment timing](https://docs.edgegap.com/learn/matchmaking)

**Cancel Matchmaking** stops the local queue but discards the remote deletion result. A cancellation event does not confirm that Edgegap removed the group.

## Parties and ready-up

Run this Game Instance flow on each player's client. The owner calls **Create Edgegap Party**, shares `GroupId`, and invitees call **Join Edgegap Party**. Each player calls **Set Party Ready**; the owner should ready last, after everyone has joined.

Use your exact profile name and an **Attributes Json** object matching its rules (`{}` only if none are required). Requests use the configured matchmaker URL and client token.

**Blueprint**

<BlueprintViewer title="Party matchmaking" src="/blueprints/crossplay-edgegap-party.txt" />

Game Instance variables: `PartyBusy`, `PartyOwner` (**Boolean**, false); `GroupId`, `MemberId` (**String**); `PartySnapshot` (**EGIK Group Up Member Response**); `PollTimer` (**Timer Handle**).

**C++**

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

```cpp
// Copyright (c) 2026 Betide Studio. All Rights Reserved.
// EdgegapPartyGameInstance.h
#pragma once
#include "Engine/GameInstance.h"
#include "Engine/EngineBaseTypes.h"
#include "Nodes/GroupUp/EGIK_CreateGroup.h"
#include "Nodes/GroupUp/EGIK_CreateGroupMember.h"
#include "Nodes/GroupUp/EGIK_UpdateGroupMember.h"
#include "Nodes/GroupUp/EGIK_GetGroupMember.h"
#include "Nodes/GroupUp/EGIK_DeleteGroup.h"
#include "Nodes/GroupUp/EGIK_DeleteGroupMember.h"
#include "TimerManager.h"
#include "EdgegapPartyGameInstance.generated.h"

UCLASS()
class UEdgegapPartyGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    virtual void Init() override;
    virtual void Shutdown() override;
    void CreateEdgegapParty(const FString &Profile, const FString &AttributesJson);
    void JoinEdgegapParty(const FString &ExistingGroupId, const FString &Profile,
                          const FString &AttributesJson);
    void SetPartyReady(bool Ready);
    void RefreshParty();
    void LeaveEdgegapParty();
    void ConnectPartyMatch();
    void StartPartyPolling();
    void ForgetPartyAfterConfirmedExpiry();
    FString GroupId;
    FString MemberId;
    FEGIK_GroupUpMemberResponse PartySnapshot;

  private:
    enum class EOperation
    {
        Create,
        Join,
        Update,
        Read,
        Delete
    };
    EOperation Operation = EOperation::Read;
    bool PartyBusy = false;
    bool PartyOwner = false;
    FTimerHandle PollTimer;
    UPROPERTY()
    TObjectPtr<UEGIK_WebRequest> Action;
    bool BeginRequest(bool WantsGroup);
    void StopPolling();
    void ApplyPartySnapshot(const FEGIK_GroupUpMemberResponse &Response);
    UFUNCTION()
    void MemberSucceeded(const FEGIK_GroupUpMemberResponse &Response, const FBetideError &Error);
    UFUNCTION()
    void MemberFailed(const FEGIK_GroupUpMemberResponse &Response, const FBetideError &Error);
    UFUNCTION()
    void DeleteSucceeded(const FBetideError &Error);
    UFUNCTION()
    void RequestFailed(const FBetideError &Error);
    void NetworkFailed(UWorld *World, UNetDriver *Driver, ENetworkFailure::Type Failure,
                       const FString &Reason);
    void TravelFailed(UWorld *World, ETravelFailure::Type Failure, const FString &Reason);
    template <class T> void StartMemberAction(T *Node, EOperation NewOperation)
    {
        Operation = NewOperation;
        Action = Node;
        Node->RegisterWithGameInstance(this);
        Node->OnSuccess.AddDynamic(this, &ThisClass::MemberSucceeded);
        Node->OnFailure.AddDynamic(this, &ThisClass::MemberFailed);
        Node->OnRateLimited.AddDynamic(this, &ThisClass::MemberFailed);
        Node->Activate();
    }
    template <class T> void StartDeleteAction(T *Node)
    {
        Operation = EOperation::Delete;
        Action = Node;
        Node->RegisterWithGameInstance(this);
        Node->OnSuccess.AddDynamic(this, &ThisClass::DeleteSucceeded);
        Node->OnFailure.AddDynamic(this, &ThisClass::RequestFailed);
        Node->OnRateLimited.AddDynamic(this, &ThisClass::RequestFailed);
        Node->Activate();
    }
};

// EdgegapPartyGameInstance.cpp
#include "EdgegapPartyGameInstance.h"
#include "Engine/Engine.h"
#include "Engine/World.h"
#include "Kismet/GameplayStatics.h"
#include "UObject/Class.h"
DEFINE_LOG_CATEGORY_STATIC(LogGameEdgegapParty, Log, All);

void UEdgegapPartyGameInstance::Init()
{
    Super::Init();
    if (GEngine)
    {
        GEngine->OnNetworkFailure().AddUObject(this, &ThisClass::NetworkFailed);
        GEngine->OnTravelFailure().AddUObject(this, &ThisClass::TravelFailed);
    }
}
void UEdgegapPartyGameInstance::Shutdown()
{
    StopPolling();
    if (GEngine)
    {
        GEngine->OnNetworkFailure().RemoveAll(this);
        GEngine->OnTravelFailure().RemoveAll(this);
    }
    Super::Shutdown();
}
bool UEdgegapPartyGameInstance::BeginRequest(bool WantsGroup)
{
    if (PartyBusy)
    {
        UE_LOG(LogGameEdgegapParty, Warning,
               TEXT("A party request is running; retry after it finishes"));
        return false;
    }
    if (WantsGroup == GroupId.IsEmpty())
    {
        UE_LOG(LogGameEdgegapParty, Warning, TEXT("%s"),
               WantsGroup ? TEXT("Join or create a group first")
                          : TEXT("Leave the current group first"));
        return false;
    }
    PartyBusy = true;
    return true;
}
void UEdgegapPartyGameInstance::CreateEdgegapParty(const FString &Profile,
                                                   const FString &AttributesJson)
{
    if (!BeginRequest(false))
        return;
    PartyOwner = true;
    FEGIK_CreateGroupRequest Request;
    Request.Profile = Profile;
    Request.Attributes = AttributesJson;
    Request.bIsReady = false;
    StartMemberAction(UEGIK_CreateGroup::CreateGroup(Request), EOperation::Create);
}
void UEdgegapPartyGameInstance::JoinEdgegapParty(const FString &ExistingGroupId,
                                                 const FString &Profile,
                                                 const FString &AttributesJson)
{
    if (!BeginRequest(false))
        return;
    PartyOwner = false;
    FEGIK_CreateGroupMemberRequest Request;
    Request.GroupId = ExistingGroupId;
    Request.Profile = Profile;
    Request.Attributes = AttributesJson;
    Request.bIsReady = false;
    StartMemberAction(UEGIK_CreateGroupMember::CreateGroupMember(Request), EOperation::Join);
}
void UEdgegapPartyGameInstance::SetPartyReady(bool Ready)
{
    if (!BeginRequest(true))
        return;
    FEGIK_UpdateGroupMemberRequest Request;
    Request.GroupId = GroupId;
    Request.MemberId = MemberId;
    Request.bIsReady = Ready;
    StartMemberAction(UEGIK_UpdateGroupMember::UpdateGroupMember(Request), EOperation::Update);
}
void UEdgegapPartyGameInstance::RefreshParty()
{
    if (!BeginRequest(true))
        return;
    FEGIK_GetGroupMemberRequest Request;
    Request.GroupId = GroupId;
    Request.MemberId = MemberId;
    StartMemberAction(UEGIK_GetGroupMember::GetGroupMember(Request), EOperation::Read);
}
void UEdgegapPartyGameInstance::StartPartyPolling()
{
    GetTimerManager().SetTimer(PollTimer, this, &ThisClass::RefreshParty, 3.0f, true);
}
void UEdgegapPartyGameInstance::StopPolling()
{
    GetTimerManager().ClearTimer(PollTimer);
}
void UEdgegapPartyGameInstance::MemberSucceeded(const FEGIK_GroupUpMemberResponse &Response,
                                                const FBetideError &)
{
    Action = nullptr;
    if (Operation == EOperation::Create || Operation == EOperation::Join)
    {
        GroupId = Response.GroupId;
        MemberId = Response.MemberId;
        StartPartyPolling();
        PartyBusy = false;
        UE_LOG(LogGameEdgegapParty, Log,
               TEXT("Party joined unready; share GroupId and ready each member"));
        return;
    }
    StartPartyPolling();
    ApplyPartySnapshot(Response);
}
void UEdgegapPartyGameInstance::ApplyPartySnapshot(const FEGIK_GroupUpMemberResponse &Response)
{
    PartySnapshot = Response;
    PartyBusy = false;
    UE_LOG(LogGameEdgegapParty, Log, TEXT("%s"), *Response.Status);
    if (Response.Status == TEXT("HOST_ASSIGNED") || Response.Status == TEXT("CANCELLED"))
        StopPolling();
}
void UEdgegapPartyGameInstance::MemberFailed(const FEGIK_GroupUpMemberResponse &,
                                             const FBetideError &Error)
{
    RequestFailed(Error);
}
void UEdgegapPartyGameInstance::RequestFailed(const FBetideError &Error)
{
    Action = nullptr;
    PartyBusy = false;
    StopPolling();
    UE_LOG(LogGameEdgegapParty, Warning, TEXT("%s"), *Error.Message);
}
void UEdgegapPartyGameInstance::LeaveEdgegapParty()
{
    if (!BeginRequest(true))
        return;
    StopPolling();
    if (PartyOwner)
    {
        FEGIK_DeleteGroupRequest Request;
        Request.GroupId = GroupId;
        StartDeleteAction(UEGIK_DeleteGroup::DeleteGroup(Request));
    }
    else
    {
        FEGIK_DeleteGroupMemberRequest Request;
        Request.GroupId = GroupId;
        Request.MemberId = MemberId;
        StartDeleteAction(UEGIK_DeleteGroupMember::DeleteGroupMember(Request));
    }
}
void UEdgegapPartyGameInstance::DeleteSucceeded(const FBetideError &)
{
    Action = nullptr;
    UE_LOG(LogGameEdgegapParty, Log, TEXT("%s"),
           PartyOwner ? TEXT("Group closed") : TEXT("Membership removed"));
    GroupId.Reset();
    MemberId.Reset();
    PartySnapshot = FEGIK_GroupUpMemberResponse();
    PartyOwner = false;
    PartyBusy = false;
}
void UEdgegapPartyGameInstance::ConnectPartyMatch()
{
    if (!BeginRequest(true))
        return;
    if (PartySnapshot.Status != TEXT("HOST_ASSIGNED"))
    {
        PartyBusy = false;
        UE_LOG(LogGameEdgegapParty, Warning, TEXT("Wait for HOST_ASSIGNED before connecting"));
        return;
    }
    StopPolling();
    FString Error;
    const bool Accepted = UEGIKBlueprintFunctionLibrary::TravelPlayerToMatchAssignment(
        UGameplayStatics::GetPlayerController(this, 0), PartySnapshot.Assignment, Error);
    PartyBusy = false;
    if (Accepted)
        UE_LOG(LogGameEdgegapParty, Log, TEXT("Travel requested; wait for the server connection"));
    else
        UE_LOG(LogGameEdgegapParty, Warning, TEXT("%s"), *Error);
}
void UEdgegapPartyGameInstance::NetworkFailed(UWorld *World, UNetDriver *,
                                              ENetworkFailure::Type Failure, const FString &)
{
    if (World && World->GetGameInstance() != this)
        return;
    UE_LOG(LogGameEdgegapParty, Warning, TEXT("%s"), *UEnum::GetValueAsString(Failure));
}
void UEdgegapPartyGameInstance::TravelFailed(UWorld *World, ETravelFailure::Type Failure,
                                             const FString &)
{
    if (World && World->GetGameInstance() != this)
        return;
    UE_LOG(LogGameEdgegapParty, Warning, TEXT("%s"), *UEnum::GetValueAsString(Failure));
}

void UEdgegapPartyGameInstance::ForgetPartyAfterConfirmedExpiry()
{
    if (PartyBusy)
    {
        UE_LOG(LogGameEdgegapParty, Warning,
               TEXT("Wait for the active request before forgetting the party"));
        return;
    }
    StopPolling();
    GroupId.Reset();
    MemberId.Reset();
    PartySnapshot = FEGIK_GroupUpMemberResponse();
    PartyOwner = false;
    UE_LOG(LogGameEdgegapParty, Log, TEXT("Local party cleared; no remote delete was sent"));
}
```

Polling runs every three seconds and stops on `HOST_ASSIGNED`, `CANCELLED`, or an error. Read `PartySnapshot` for readiness, ticket ID, status, and assignment. After a transient error or rate limit, wait before calling **Refresh Party** to resume. A busy request must finish before another operation can start.

Call **Connect Party Match** after `HOST_ASSIGNED`, using an IP-compatible GameNetDriver and a local Player Controller. Success only means travel was requested; **NetworkError** and **TravelError** report later failures. Retry connection only after failure, with a limit appropriate for your server startup time. Use the member's ticket ID in your game's admission flow if required; travel does not send it automatically.

**Leave Edgegap Party** deletes the whole group for its creator and only the membership for an invitee. IDs stay available if deletion fails. After confirming that a membership expired or was removed, **Forget Party After Confirmed Expiry** clears local state; it does not delete anything remotely. Leave before shutting down the game rather than relying on an exit-time request.

The unified adapter's party ready-up path remains incomplete; this example uses the direct Group Up nodes.

## Attributes and placement

Match the profile's attribute names and types. Group Up offers **Structured Attributes** or a raw JSON **Attributes** object; use one, not both. **Player Ip** is available on the direct group/member requests.

The unified adapter sends `Config.Attributes` as shared attributes. It does not serialize `Config.LatencyHints` or player IPs into its group requests. If your profile needs location or latency data, provide the profile's expected attributes through the direct Group Up flow. Region labels alone are not measured latency.

<Accordion>
  <AccordionItem title="Backfill an assigned server">

This dedicated-server Game Instance handles one team's backfill at a time. Pass **Start Team Backfill** an **EGIK Create Back Fill Request**:

| Input | Value |
| --- | --- |
| Profile | Your backfill-compatible matchmaker profile |
| Deployment Request ID | The running deployment's ID |
| Assignment Details | Its full assignment JSON: `fqdn`, `public_ip`, `ports`, `location` |
| Tickets | At least one connected team member: ticket ID → full ticket JSON, not only attributes |
| Attributes Override | Optional profile attributes; omit the reserved `assignment` key |

For a profile using `backfill_group_size`, update the existing tickets' values to the group sizes that fit the team's remaining seats. Use actual roster data from the matchmaker's injected variables and prior backfills. [Edgegap backfill rules](https://docs.edgegap.com/learn/matchmaking/matchmaker-in-depth#backfill)

Leave **Matchmaking URL** and **Auth Token** empty to use configuration. If overridden, this example keeps them for polling and deletion.

**Blueprint**

<BlueprintViewer title="Replace missing players" src="/blueprints/crossplay-edgegap-backfill.txt" />

Game Instance variables: `BackfillBusy` (**Boolean**, false); `BackfillId`, `BackfillStatus` (**String**); `ActiveRequest` (**EGIK Create Back Fill Request**); `AssignedTicket` (**EGIK Matchmaking Response**); `BackfillTimer` (**Timer Handle**).

**C++**

Server module dependencies: `EdgegapIntegrationKit`, `BetideCore`, `Engine`.

```cpp
// Copyright (c) 2026 Betide Studio. All Rights Reserved.
// EdgegapBackfillGameInstance.h
#pragma once
#include "Engine/GameInstance.h"
#include "Nodes/Backfills/EGIK_CreateBackfill.h"
#include "Nodes/Backfills/EGIK_GetBackFillTicketInformation.h"
#include "Nodes/Backfills/EGIK_DeleteBackFillTicket.h"
#include "TimerManager.h"
#include "EdgegapBackfillGameInstance.generated.h"

UCLASS()
class UEdgegapBackfillGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    virtual void Shutdown() override;
    void StartTeamBackfill(const FEGIK_CreateBackFillRequest &Request);
    void RefreshBackfill();
    void DeleteTeamBackfill();
    void ForgetBackfillAfterConfirmedRemoval();
    FString BackfillId;
    FString BackfillStatus;
    FEGIK_MatchmakingResponse AssignedTicket;

  private:
    bool BackfillBusy = false;
    FTimerHandle BackfillTimer;
    FEGIK_CreateBackFillRequest ActiveRequest;
    UPROPERTY()
    TObjectPtr<UEGIK_WebRequest> Action;
    bool BeginRequest(bool NeedsId);
    void ApplyBackfillResult(const FString &Id, const FString &Status,
                             const FEGIK_MatchmakingResponse &Replacement);
    void ClearLocalBackfill();
    UFUNCTION()
    void Created(const FEGIK_CreateBackFillResponse &Response, const FBetideError &Error);
    UFUNCTION()
    void CreateFailed(const FEGIK_CreateBackFillResponse &Response, const FBetideError &Error);
    UFUNCTION()
    void Refreshed(const FEGIK_GetBackFillTicketInformationResponse &Response,
                   const FBetideError &Error);
    UFUNCTION()
    void RefreshFailed(const FEGIK_GetBackFillTicketInformationResponse &Response,
                       const FBetideError &Error);
    UFUNCTION()
    void Deleted(const FBetideError &Error);
    UFUNCTION()
    void RequestFailed(const FBetideError &Error);
};

// EdgegapBackfillGameInstance.cpp
#include "EdgegapBackfillGameInstance.h"
#include "Kismet/KismetSystemLibrary.h"
DEFINE_LOG_CATEGORY_STATIC(LogGameEdgegapBackfill, Log, All);

void UEdgegapBackfillGameInstance::Shutdown()
{
    GetTimerManager().ClearTimer(BackfillTimer);
    Super::Shutdown();
}
bool UEdgegapBackfillGameInstance::BeginRequest(bool NeedsId)
{
    if (!UKismetSystemLibrary::IsDedicatedServer(this))
    {
        UE_LOG(LogGameEdgegapBackfill, Warning, TEXT("Run backfill on the dedicated server"));
        return false;
    }
    if (BackfillBusy)
    {
        UE_LOG(LogGameEdgegapBackfill, Warning,
               TEXT("Backfill request running; retry after completion"));
        return false;
    }
    if (NeedsId == BackfillId.IsEmpty())
    {
        UE_LOG(LogGameEdgegapBackfill, Warning, TEXT("%s"),
               NeedsId ? TEXT("No active backfill ID")
                       : TEXT("Delete the current backfill before creating another"));
        return false;
    }
    BackfillBusy = true;
    return true;
}
void UEdgegapBackfillGameInstance::StartTeamBackfill(const FEGIK_CreateBackFillRequest &Request)
{
    if (!BeginRequest(false))
        return;
    ActiveRequest = Request;
    if (Request.Tickets.IsEmpty())
    {
        BackfillBusy = false;
        UE_LOG(LogGameEdgegapBackfill, Warning,
               TEXT("Supply at least one current team ticket with its full JSON data"));
        return;
    }
    auto *Node = UEGIK_CreateBackfill::CreateBackFillRequest(Request);
    Action = Node;
    Node->RegisterWithGameInstance(this);
    Node->OnSuccess.AddDynamic(this, &ThisClass::Created);
    Node->OnFailure.AddDynamic(this, &ThisClass::CreateFailed);
    Node->OnRateLimited.AddDynamic(this, &ThisClass::CreateFailed);
    Node->Activate();
}
void UEdgegapBackfillGameInstance::Created(const FEGIK_CreateBackFillResponse &Response,
                                           const FBetideError &)
{
    ApplyBackfillResult(Response.Id, Response.Status, Response.AssignedTicket);
}
void UEdgegapBackfillGameInstance::CreateFailed(const FEGIK_CreateBackFillResponse &,
                                                const FBetideError &Error)
{
    RequestFailed(Error);
}
void UEdgegapBackfillGameInstance::RefreshBackfill()
{
    if (!BeginRequest(true))
        return;
    auto *Node = UEGIK_GetBackFillTicketInformation::GetBackFillTicketInformation(
        BackfillId, ActiveRequest.MatchmakingURL, ActiveRequest.AuthToken);
    Action = Node;
    Node->RegisterWithGameInstance(this);
    Node->OnSuccess.AddDynamic(this, &ThisClass::Refreshed);
    Node->OnFailure.AddDynamic(this, &ThisClass::RefreshFailed);
    Node->OnRateLimited.AddDynamic(this, &ThisClass::RefreshFailed);
    Node->Activate();
}
void UEdgegapBackfillGameInstance::Refreshed(
    const FEGIK_GetBackFillTicketInformationResponse &Response, const FBetideError &)
{
    ApplyBackfillResult(Response.Id, Response.Status, Response.AssignedTicket);
}
void UEdgegapBackfillGameInstance::RefreshFailed(const FEGIK_GetBackFillTicketInformationResponse &,
                                                 const FBetideError &Error)
{
    RequestFailed(Error);
}
void UEdgegapBackfillGameInstance::ApplyBackfillResult(const FString &Id, const FString &Status,
                                                       const FEGIK_MatchmakingResponse &Replacement)
{
    Action = nullptr;
    BackfillId = Id;
    BackfillStatus = Status;
    AssignedTicket = Replacement;
    BackfillBusy = false;
    if (!Replacement.TicketId.IsEmpty())
    {
        GetTimerManager().ClearTimer(BackfillTimer);
        UE_LOG(LogGameEdgegapBackfill, Log,
               TEXT("Replacement ticket available; verify its player connection before counting "
                    "the seat as filled"));
        return;
    }
    if (Status == TEXT("CANCELLED"))
    {
        GetTimerManager().ClearTimer(BackfillTimer);
        UE_LOG(LogGameEdgegapBackfill, Warning,
               TEXT("Backfill cancelled; clear expired state only after confirming removal"));
        return;
    }
    GetTimerManager().SetTimer(BackfillTimer, this, &ThisClass::RefreshBackfill, 3.0f, true);
}
void UEdgegapBackfillGameInstance::RequestFailed(const FBetideError &Error)
{
    Action = nullptr;
    BackfillBusy = false;
    GetTimerManager().ClearTimer(BackfillTimer);
    UE_LOG(LogGameEdgegapBackfill, Warning, TEXT("%s"), *Error.Message);
}
void UEdgegapBackfillGameInstance::DeleteTeamBackfill()
{
    if (!BeginRequest(true))
        return;
    GetTimerManager().ClearTimer(BackfillTimer);
    auto *Node = UEGIK_DeleteBackFillTicket::DeleteBackFillTicket(
        BackfillId, ActiveRequest.MatchmakingURL, ActiveRequest.AuthToken);
    Action = Node;
    Node->RegisterWithGameInstance(this);
    Node->OnSuccess.AddDynamic(this, &ThisClass::Deleted);
    Node->OnFailure.AddDynamic(this, &ThisClass::RequestFailed);
    Node->OnRateLimited.AddDynamic(this, &ThisClass::RequestFailed);
    Node->Activate();
}
void UEdgegapBackfillGameInstance::ClearLocalBackfill()
{
    GetTimerManager().ClearTimer(BackfillTimer);
    BackfillId.Reset();
    BackfillStatus.Reset();
    AssignedTicket = FEGIK_MatchmakingResponse();
    ActiveRequest = FEGIK_CreateBackFillRequest();
}
void UEdgegapBackfillGameInstance::Deleted(const FBetideError &)
{
    Action = nullptr;
    ClearLocalBackfill();
    BackfillBusy = false;
    UE_LOG(LogGameEdgegapBackfill, Log, TEXT("Backfill removed"));
}
void UEdgegapBackfillGameInstance::ForgetBackfillAfterConfirmedRemoval()
{
    if (BackfillBusy)
    {
        UE_LOG(LogGameEdgegapBackfill, Warning, TEXT("Wait for the active request first"));
        return;
    }
    ClearLocalBackfill();
    UE_LOG(LogGameEdgegapBackfill, Log, TEXT("Local backfill cleared; no remote delete was sent"));
}
```

Polling stops when `AssignedTicket` is available, the backfill is cancelled, or a request fails. Verify the arriving player's ticket against your server's admission data; assignment alone does not fill a seat. Copy the assigned ticket into your roster before deleting or clearing this flow's state.

Call **Delete Team Backfill** when the match closes or the request is no longer needed. Failure retains the ID for retry. After a transient error or rate limit, wait before **Refresh Backfill**. Use **Forget Backfill After Confirmed Removal** only after confirming expiry/removal; it clears local data without a remote delete. To fill further seats, start a new request with the updated roster.

  </AccordionItem>
  <AccordionItem title="Cancellation, recovery, and errors">

For unified queues, **Cancel Matchmaking** deletes the adapter's group. For direct Group Up, remove a departing member with **Delete Group Member**; **Delete Group** ends the whole group.

The unified adapter caches the group's owner-member ID in memory. A saved group ID alone cannot resume its polling after restarting the game. For persistent party recovery, retain the direct API's group and member IDs and handle expiry.

If stuck in `SEARCHING`, check profile criteria, enough compatible players, and each member's readiness. If stuck in `MATCH_FOUND`, check deployment capacity and server startup. An expired or deleted ticket needs a new request. Show the returned `FBetideError` and back off after rate limits instead of polling every frame.

  </AccordionItem>
</Accordion>
