Skip to main content

Subscriptions and cross-device sync

A subscription is a server-side record on the player's CurseForge account. Subscribe to a mod on one device and every other device running your game can install it without the player finding it again. Unsubscribing propagates the same way.

Subscriptions are where the local mod library is reconciled against that server-side list. The feature has its own interface (ICFCoreSubscription in C++, the cfcore|Subscription palette category in Blueprint), its own settings block (FCFCoreSettings::subscriptions), and its own concept of a sync plan: an inspectable list of the install, update, and uninstall actions needed to make the local library match the server. This page documents cfcore-sdk-ue.

info

Four conditions must hold.

PrerequisiteHow it fails if missing
Subscriptions enabled for your game in the Developer Portal at console.curseforge.comCalls fail with HTTP errors, surfaced as ECFCoreErrorCodes::ApiError. Contact cfforstudios@overwolf.com to have them enabled
SDK initializedEvery node here with an on_error pin fires it with ECFCoreErrorCodes::FailedToInitialize and description Not initialized. Disable Auto Management has no on_error pin and returns silently
Player authenticatedICFCoreSubscription::Subscribe, Unsubscribe, GetModsSubscriptions, and plan building check IsAuthenticated and fail with ECFCoreErrorCodes::UserNotAuthenticated before any network call. ExecuteSyncPlan does not check, and no cfcore|Api Authorized node checks
Calls made on the game threadSyncSubscriptions and ExecuteSyncPlan assert with check(IsInGameThread()), which halts non-shipping builds

Confirm enablement at runtime by reading FGame::supportedFeatures.supportModSubscriptions from the Get Game Info node in cfcore|Api (C++: ICFCoreApi::GetGame). It defaults to false and can differ between your dev and live game records. See Setting up your game.

In C++ the interface comes from CFCoreContext::GetInstance()->Subscription(), a raw pointer. Check it for null: that check is what the Blueprint wrappers report as FailedToInitialize. Subscription callbacks arrive on the game thread.

Subscribing and unsubscribing

ICFCoreSubscription::Subscribe and Unsubscribe do more than call the API.

  1. The SDK checks IsAuthenticated.
  2. It calls the authorized API to create or remove the server-side subscription.
  3. Only if step 2 succeeded, it installs the mod (Subscribe) or uninstalls it (Unsubscribe).
  4. The callback fires once, after both steps.

A failed API call leaves the local library untouched. A failed install after a successful subscribe leaves the player subscribed but not installed, which the next sync picks up as an Install action.

#include <cfcore_context.h>
#include <subscription/cfcore_subscription.h>

using namespace cfcore;

FSubscribeRequest request;
request.modId = mod_id;

CFCoreContext::GetInstance()->Subscription()->Subscribe(
request,
ICFCoreSubscription::FSubscribeDelegate::CreateLambda(
[](const FCFCoreMod& mod, const TOptional<FCFCoreError>& opt_err) {
if (opt_err.IsSet() && opt_err->isError) {
return; // mod is default constructed on this path.
}
// Subscribed and installed: mod.id and mod.name are populated.
})
);

Unsubscribe takes the same FSubscribeRequest, and its FUnsubscribeDelegate carries only the error optional.

Blueprint: use Subscribe and Unsubscribe in cfcore|Subscription. Both take a request pin (an FSubscribeRequest, whose only field is modId) and expose mutually exclusive on_success and on_error pins. Subscribe's on_success carries the installed mod.

info

Neither function accepts a progress delegate, because Subscribe passes an empty FInstallProgressDelegate to the library. For a progress bar, bind the OnModInstallProgress and OnModInstalled assignable delegates on UCFCoreSubsystem (C++: the same-named ICFCoreLibrary accessors return multicast delegate references) and correlate on FLibraryProgress::modId.

note

Subscribe installs through the three-argument ICFCoreLibrary::Install overload, so its origin stays at the default EModInstallOrigin::ModPage. Only sync-driven installs are tagged EModInstallOrigin::SubscribeModule.

Two Subscribe nodes, two Unsubscribe nodes

Two Blueprint nodes display as Subscribe and two display as Unsubscribe, separated only by palette category. Picking the wrong one is a silent bug: the server state changes either way, the disk state does not.

Blueprint nodeCategorySubsystem functionInterface methodWhat it does
Subscribecfcore|SubscriptionSubscribeModICFCoreSubscription::SubscribeSubscribes, then installs if the subscribe succeeded
Unsubscribecfcore|SubscriptionUnsubscribeModICFCoreSubscription::UnsubscribeUnsubscribes, then uninstalls if the unsubscribe succeeded
Subscribecfcore|Api AuthorizedApiSubscribeICFCoreApiAuthorized::SubscribeServer call only. Local library untouched
Unsubscribecfcore|Api AuthorizedApiUnsubscribeICFCoreApiAuthorized::UnsubscribeServer call only. Files stay installed

Use the cfcore|Subscription pair for the standard behaviour. Use the cfcore|Api Authorized pair when your game owns install timing, ordering, or progress UI, or when the player should keep the files after unsubscribing.

warning

Do not mix layers for one player action, because calling both issues two subscribe requests. And if automatic management is on with allowInstallActions at its default of true, an API-layer Subscribe with no matching install is picked up by the next sync run and installed anyway, on the timer rather than immediately.

Choosing a sync strategy

Sync reconciles the locally installed mods with the server-side subscription list. There are three ways to run it.

StrategyC++ on ICFCoreSubscriptionBlueprint nodeUse it when
One-shot syncSyncSubscriptionsSync SubscriptionsYou want the library reconciled and do not need to see the plan. The recommended startup call
Inspect, then executeGetSyncPlan, then ExecuteSyncPlanGet Sync Plan, then Execute Sync PlanYou want to show the player what will change, ask for confirmation, or apply only part of the plan
Automatic background managementEnableAutoManagement, DisableAutoManagementEnable Auto Management, Disable Auto ManagementThe player is in a mod browser, menu, or launcher and you want the library to keep itself current with no further calls

The SDK's own guidance: SyncSubscriptions once at startup, GetSyncPlan plus ExecuteSyncPlan behind any confirmation UI, automatic management only while the player is in a menu or browser. Neither planning call is cheap, so never call one on tick.

How a sync plan is built

Every strategy plans the same way: check authentication, read the installed mods, fetch every page of the subscription list, refresh the stored details of installed mods from that data, then diff and emit one FSubscriptionSyncPlanItem per required action.

Local stateServer stateActionEmitted only if
Not installedSubscribedInstallallowInstallActions
Installed, installedFile.id differs from the id of the last entry in the subscribed mod's latestFilesSubscribedUpdateallowUpdateActions
Installed and already on that fileSubscribedNo itemNothing to do
Installed, subscribed mod has an empty latestFiles arraySubscribedNo itemNo target file can be determined
InstalledNot subscribedUninstallallowUninstallActions. If false, the whole uninstall pass is skipped

Two classes of installed mod are skipped in both the update and the uninstall passes.

Skipped whenWhy
FInstalledMod::dynamicContent is trueDynamic content is downloaded automatically by the game and is not treated as installed until the player installs it manually, for example shared cosmetics on a server. Subscriptions never manage it. This skip is unconditional
FInstalledMod::unmanaged is true and FCFCoreSettings::unmanagedMods.enabled is trueUnmanaged mods sit in the mods directory without having been installed through the plugin, which lets mod authors play with a creation before uploading it
warning

FCFCoreSettingsUnmanagedMods::enabled defaults to false, so that second skip is inactive on default settings and a locally built mod is a candidate for Uninstall. Turn it on if mod authors will run your game with local work in the mods directory.

warning

The three allow* settings filter plan building only. They are not re-checked at execution time, so a plan you added items to, or built yourself, runs in full.

Sync actions and item status

ESubscriptionSyncAction is a uint8 UENUM(BlueprintType).

ValueNumericMeaning
Install0Subscribed but not installed locally. Install it
Update1Subscribed and installed, but not on the target file. Replace it
Uninstall2Installed locally but no longer subscribed. Remove it

Install and Update use the same install call, so the distinction exists only so your UI can say "updating" instead of "installing". Both pass FInstallModAdditionalParams::origin as EModInstallOrigin::SubscribeModule.

ESubscriptionSyncItemStatus is a uint8 UENUM(BlueprintType) and is binary. There is no in-progress value, because a result item only exists after its action has finished.

ValueNumericMeaning
Success0The action completed. The item's error field is a non-error FCFCoreError
Failed1The action failed. The item's error field carries the reason

Plan and result models

FSubscriptionSyncPlanItem is a BlueprintType USTRUCT with BlueprintReadOnly fields. A plan is an array of these.

FieldTypeDefaultMeaning
actionESubscriptionSyncActionInstallThe action the SDK intends to take
modFCFCoreModemptyThe full mod record, so a confirmation dialog can render name, authors, logo, and latestFiles (each FFile carries fileLength) without a second query

FSubscriptionSyncResultItem is what execution returns, one per plan item, in plan order.

FieldTypeDefaultMeaning
modIdint640The mod the action applied to. A raw ID, not an FCFCoreMod: keep your plan if you need names for a results screen
actionESubscriptionSyncActionInstallThe action that was attempted
statusESubscriptionSyncItemStatusSuccessWhether that action succeeded
errorFCFCoreErrorclearedPopulated when status is Failed. Check isError, then code and description

One-shot sync

SyncSubscriptions builds the current plan and applies it in one call. A plan that fails to build reports the error with an empty result array, and a plan with zero items fires the callback with an empty array and no error, without touching the library.

Blueprint: use Sync Subscriptions in cfcore|Subscription. on_success carries result_items, on_error carries an FCFCoreError.

warning

on_success firing does not mean every mod synced. Per-mod failures are reported per item, never as a top-level error, so always iterate result_items and check status.

Inspect, plan, then execute

GetSyncPlan runs the same checks and diff, then stops. Filter or present the returned array, remove items freely, and pass what you want applied to ExecuteSyncPlan. Match results back to your plan by modId and action.

ICFCoreSubscription* subscription =
CFCoreContext::GetInstance()->Subscription();

subscription->GetSyncPlan(
ICFCoreSubscription::FGetSyncPlanDelegate::CreateLambda(
[subscription](const TArray<FSubscriptionSyncPlanItem>& plan_items,
const TOptional<FCFCoreError>& opt_err) {
if (opt_err.IsSet() && opt_err->isError) {
return;
}

// Apply updates now, defer installs and uninstalls to a safer moment.
TArray<FSubscriptionSyncPlanItem> updates_only;
for (const FSubscriptionSyncPlanItem& item : plan_items) {
if (item.action == ESubscriptionSyncAction::Update) {
updates_only.Add(item);
}
}

subscription->ExecuteSyncPlan(
updates_only,
ICFCoreSubscription::FExecuteSyncPlanDelegate::CreateLambda(
[](const TArray<FSubscriptionSyncResultItem>& results,
const TOptional<FCFCoreError>& opt_err) {
// Check results[i].status per item.
})
);
})
);

Blueprint: use Get Sync Plan in cfcore|Subscription (on_success carries plan_items), then Execute Sync Plan in the same category, which takes a plan_items array input and returns result_items.

BehaviourWhat it means for your integration
Actions run in parallelAll items are dispatched together and the callback waits for the last one. Do not assume uninstalls happen before installs, or that disk space freed by an uninstall is available to a concurrent install in the same plan
One execution at a timeA second ExecuteSyncPlan or SyncSubscriptions during an active execution is rejected with ECFCoreErrorCodes::SubscriptionSyncInProgress. GetSyncPlan is not covered by the guard. The guard sits at execution, not entry, so a rejected SyncSubscriptions has already fetched every subscription page. Gate the call in your own code
A stale plan is not revalidatedExecuteSyncPlan applies what you hand it. If the player unsubscribed on another device between your two calls, the plan still installs the mod. Keep the gap short, or re-plan after a long confirmation dialog

Automatic background management

EnableAutoManagement puts the sync on a repeating timer.

  1. On enable, the SDK reads automaticModManagementIntervalMs. Enabling while already enabled disables first, then restarts with that interval, which is how a settings change takes effect.
  2. The first timer is one interval away. Nothing syncs immediately. If the library must be correct before the player can act, call SyncSubscriptions yourself as well.
  3. Each run is a full plan build and execution, respecting the allow* settings.
  4. The next run is scheduled only after the previous one finishes, so runs never overlap and the real cadence is the interval plus the last sync's duration.
  5. DisableAutoManagement cancels a pending timer. A sync already in progress finishes, and nothing further is scheduled.
CFCoreContext::GetInstance()->Subscription()->EnableAutoManagement(
ICFCoreSubscription::FAutoManagementDelegate::CreateLambda(
[](const TOptional<FCFCoreError>& opt_err) {
// Management started. Sync outcomes are not reported here.
})
);

// When the player leaves the menu:
CFCoreContext::GetInstance()->Subscription()->DisableAutoManagement();

Blueprint: use Enable Auto Management in cfcore|Subscription, with on_success and on_error pins. Disable Auto Management in the same category has no data pins, is safe to call when management is not enabled, and returns silently when the SDK is not initialized.

warning

The C++ EnableAutoManagement always invokes its delegate with no error, and the Blueprint node fires on_error only for the not-initialized case. The callback tells you management started, not the outcome of any sync. Automatic runs report nothing back, so surface progress and failures through OnModInstallProgress, OnModInstalled, and ICFCoreLibrary::GetInstalledMods.

Do not run automatic management during active gameplay

This is a hard rule, not a performance suggestion. Management installs, updates, and uninstalls content on a timer with no coordination with your game's use of that content, so during gameplay it can delete or replace files your game currently has loaded.

  1. Enable management when the player enters a menu, launcher, or mod browser.
  2. Call DisableAutoManagement before the player starts a session, loads a world, or a match begins.
  3. Load mods from disk at your usual gate with ICFCoreLibrary::GetInstalledMods, after management is off.
  4. To reconcile mid-session, use GetSyncPlan and execute only the subset you know is safe, which in practice means nothing currently loaded.

DisableAutoManagement returns early when management is not enabled, so call it defensively on every transition into gameplay rather than tracking whether you enabled it.

Subscription settings

The block lives at FCFCoreSettings::subscriptions as an FCFCoreSettingsSubscriptions. Every field is BlueprintReadWrite, EditAnywhere in the cfcore category.

SettingTypeDefaultEffect
automaticModManagementIntervalMsint3230000Milliseconds between automatic runs. Declared valid range 10000 to 600000 (10 seconds to 10 minutes), enforced as ClampMin and ClampMax on the editor property only, so a value set in code is not clamped at runtime. Read when EnableAutoManagement is called
allowInstallActionsbooltruePermits Install items in generated sync plans
allowUninstallActionsbooltruePermits Uninstall items in generated sync plans
allowUpdateActionsbooltruePermits Update items in generated sync plans
autoSubscribeInstalledModsboolfalseOn the first login after initialization, subscribes the player to any locally installed managed mod missing from their server-side subscription list

The three allow* flags are read fresh on every plan build, so they gate GetSyncPlan, SyncSubscriptions, and every automatic run, but not ExecuteSyncPlan.

FCFCoreSettings settings;
settings.subscriptions.allowUpdateActions = true;
settings.subscriptions.allowInstallActions = false;
settings.subscriptions.allowUninstallActions = false;
settings.subscriptions.automaticModManagementIntervalMs = 60000;

CFCoreContext::GetInstance()->Initialize(settings, /* delegate */ {});

Blueprint: set these under Project Settings > Plugins > CFCore, or build an FCFCoreSettings and feed it to the Initialize node.

The update-only safety option. The configuration above is the low-risk shape for background management: allow updates, nothing else. Plans then contain only Update items, so management refreshes content the player already has and never adds or removes mods behind their back, and you apply new subscriptions and removals at a moment you choose. The SDK recommends this for lower-risk background checks, which makes it a reasonable default for any game that keeps management on outside a dedicated mod screen.

autoSubscribeInstalledMods covers the migration case: a player who had mods installed before your game had subscriptions, or before they signed in. On the first login after initialization the SDK walks every page of GetModsSubscriptionsIds, diffs against installed mods excluding any with unmanaged set, and subscribes the player to anything installed but not subscribed, through SubscribeMods in batches of at most 50 IDs. A stored per-user flag means it runs once per player, and that flag is also set when a batch subscribe fails, so a failed run is not retried. Leave it off if your game manages subscriptions itself.

API-level surface

ICFCoreApiAuthorized is the transport: it talks to the subscription endpoints and does nothing else, with no authentication pre-check and no library action. Reach it in C++ with CFCoreContext::GetInstance()->Api()->Authorized(), which returns a TSharedPtr. Subscribe and Unsubscribe also exist at this layer, covered above in Two Subscribe nodes, two Unsubscribe nodes. The three methods below have no equivalent at the ICFCoreSubscription layer that covers the same case: an ids-only listing, subscribing many mods in one request, or listing subscriptions in Blueprint, which has no subscription-layer node.

MethodEndpointRequest modelDelegate payload
GetModsSubscriptionsGET /v1/users/me/mods/subscriptionsFCFCoreApiRequestPaginationTOptional<TArray<FCFCoreMod>>, TOptional<FCFCoreApiResponsePagination>, TOptional<FCFCoreApiResponseError>
GetModsSubscriptionsIdsGET /v1/users/me/mods/subscriptions/idsFCFCoreApiRequestPaginationTOptional<TArray<int64>>, TOptional<FCFCoreApiResponsePagination>, TOptional<FCFCoreApiResponseError>
SubscribeModsPOST /v1/users/me/mods/subscriptions/subscribeFSubscribeModsRequestTOptional<FCFCoreApiResponseError>

Request models, all USTRUCT(BlueprintType) with BlueprintReadWrite fields.

StructFieldTypeDefault
FSubscribeModsRequestmodIdsTArray<int64>empty
FCFCoreApiRequestPaginationindexint320
FCFCoreApiRequestPaginationpageSizeint3220

FCFCoreApiResponsePagination, returned by both listing methods. All four fields are int32 and BlueprintReadOnly.

FieldMeaning
indexZero-based index of the first record in this page. A record offset, not a page number
pageSizeNumber of records the server used for this page
resultCountNumber of records actually returned in this page
totalCountTotal records available across all pages
danger

The payload optionals are only guaranteed to be set when the error optional is unset. Check the error first and return, in every handler, or you will dereference an unset TOptional.

Listing subscribed mods

GetModsSubscriptions returns full FCFCoreMod records: use it for a "My subscriptions" tab that renders names, categories, and files. GetModsSubscriptionsIds returns TArray<int64>: use it for a reconciliation pass that only compares two sets of IDs.

  1. Build an FCFCoreApiRequestPagination with index set to 0 and call either method.
  2. In the delegate, check the error optional first, then append the page payload to your accumulator.
  3. Compute the next index as the response index plus the response pageSize. If it is greater than or equal to totalCount you have every record, otherwise call again with it. Stop if the response pageSize is 0 or the computed next index does not advance, since a non-advancing index means the loop will not terminate on its own.

That is the loop the SDK runs internally for plan building, so a large subscription list costs several round trips per sync.

void UMySubscriptionList::FetchPage(int32 index) {
cfcore::FCFCoreApiRequestPagination pagination;
pagination.index = index;

cfcore::CFCoreContext::GetInstance()->Api()->Authorized()
->GetModsSubscriptions(
pagination,
cfcore::ICFCoreApiAuthorized::FGetModsSubscriptionsDelegate::
CreateUObject(this, &UMySubscriptionList::OnPage));
}

void UMySubscriptionList::OnPage(
const TOptional<TArray<cfcore::FCFCoreMod>>& opt_mods,
const TOptional<cfcore::FCFCoreApiResponsePagination>& opt_page,
const TOptional<cfcore::FCFCoreApiResponseError>& opt_error) {

if (opt_error.IsSet()) {
return;
}

Subscriptions.Append(*opt_mods);

const int32 next_index = opt_page->index + opt_page->pageSize;
if (next_index >= opt_page->totalCount) {
OnAllSubscriptionsLoaded();
return;
}
FetchPage(next_index);
}

Blueprint: use Get Mods Subscriptions or Get Mods Subscriptions Ids in cfcore|Api Authorized. Both take a pagination input pin and expose on_error. The first fires on_subscriptions with mods and pagination; the second fires on_subscriptions_ids with ids and pagination. In C++, keep the receiver alive until the delegate fires: prefer CreateUObject or CreateWeakLambda over a bare CreateLambda that captures this.

info

ICFCoreSubscription::GetModsSubscriptions hits the same endpoint but pre-checks authentication and delivers non-optional payloads with FCFCoreError. It has no Blueprint node, so in Blueprint, listing subscriptions is API-layer only.

Batch subscribing

SubscribeMods takes an array of mod IDs in one request. Its documented maximum is 50 mods per call. The delegate carries only an error optional, so success means the whole batch was accepted and there is no per-mod result. Stop on the first failed batch rather than continuing with later chunks.

warning

The SDK does not clamp the array at this layer, it posts whatever you put in modIds with no length check. Max 50 mods per call, so chunk it yourself.

#include <api/models/subscribe_mods_request.h>

static constexpr int32 kMaxSubscribeBatch = 50;

const int32 batch_size = FMath::Min(kMaxSubscribeBatch, mod_ids.Num());

cfcore::FSubscribeModsRequest request;
request.modIds.Append(mod_ids.GetData(), batch_size);
mod_ids.RemoveAt(0, batch_size);
// Call SubscribeMods, then recurse with the remaining ids on success.

Blueprint: use Subscribe Mods in cfcore|Api Authorized. Its request pin is an FSubscribeModsRequest, whose only pin is the modIds array, and on_success carries no payload pins. There is no batch equivalent in cfcore|Subscription and no bulk unsubscribe anywhere in the SDK, so batch subscribing is API-layer only and removals are one call each. SubscribeMods installs nothing: follow it with a sync if you want the local library updated.

Error handling

The subscription interface reports FCFCoreError in C++: check isError, branch on code, use description for logs only.

CodeRaised byMeaning and recovery
FailedToInitializeEvery node here with an on_error pin, and any call where the interface pointer is nullDescription is Not initialized. Initialize first. Disable Auto Management returns silently instead
UserNotAuthenticatedICFCoreSubscription::Subscribe, Unsubscribe, GetModsSubscriptions, plan buildingNo authenticated player and no network call made. Send them through authentication, then retry. No cfcore|Api Authorized node raises this: it returns ApiError instead
SubscriptionSyncInProgressSyncSubscriptions, ExecuteSyncPlanAnother execution is running. Wait for its callback, do not poll in a tight loop
ApiErrorAny call that reaches the serverThe server rejected the request, or a raw API entry point was called without a valid token. This is also what you see when subscriptions are not enabled for your game. Read apiError for detail, and contact cfforstudios@overwolf.com if your Developer Portal configuration looks correct

In C++ the API layer reports FCFCoreApiResponseError instead, whose tokenExpired flag is set on HTTP 401 and means "no usable token", including a player who was never authenticated. Every cfcore|Api Authorized Blueprint node converts that into an FCFCoreError with code set to ApiError and the original nested in apiError, so to branch on a specific flag in Blueprint, break error, then break error.apiError.

Per-mod failures inside a sync never surface as a top-level error. They appear as FSubscriptionSyncResultItem entries with status set to Failed, and a sync where every item failed still calls on_success.

Next steps

Load mods with ICFCoreLibrary::GetInstalledMods (Blueprint: Get Installed Mods in cfcore|Library) at your chosen gate, and check each entry's status, enabled, and pathOnDisk before loading. Set enabled state and load order with Update Installed Mods Properties.

note

The mod browser ships subscription-related widgets, but its UCFCoreUISubsystem exposes no subscribe, unsubscribe, or sync API of its own, so a studio using that UI still wires the cfcore|Subscription nodes on this page itself.

Authentication concepts are covered in Authentication overview and Sessions and account management. To have subscriptions enabled, or if a sync fails with ApiError and your configuration in the Developer Portal at console.curseforge.com looks correct, contact cfforstudios@overwolf.com.