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.
Four conditions must hold.
| Prerequisite | How it fails if missing |
|---|---|
| Subscriptions enabled for your game in the Developer Portal at console.curseforge.com | Calls fail with HTTP errors, surfaced as ECFCoreErrorCodes::ApiError. Contact cfforstudios@overwolf.com to have them enabled |
| SDK initialized | Every 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 authenticated | ICFCoreSubscription::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 thread | SyncSubscriptions 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.
- The SDK checks
IsAuthenticated. - It calls the authorized API to create or remove the server-side subscription.
- Only if step 2 succeeded, it installs the mod (
Subscribe) or uninstalls it (Unsubscribe). - 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.
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.
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 node | Category | Subsystem function | Interface method | What it does |
|---|---|---|---|---|
| Subscribe | cfcore|Subscription | SubscribeMod | ICFCoreSubscription::Subscribe | Subscribes, then installs if the subscribe succeeded |
| Unsubscribe | cfcore|Subscription | UnsubscribeMod | ICFCoreSubscription::Unsubscribe | Unsubscribes, then uninstalls if the unsubscribe succeeded |
| Subscribe | cfcore|Api Authorized | ApiSubscribe | ICFCoreApiAuthorized::Subscribe | Server call only. Local library untouched |
| Unsubscribe | cfcore|Api Authorized | ApiUnsubscribe | ICFCoreApiAuthorized::Unsubscribe | Server 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.
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.
| Strategy | C++ on ICFCoreSubscription | Blueprint node | Use it when |
|---|---|---|---|
| One-shot sync | SyncSubscriptions | Sync Subscriptions | You want the library reconciled and do not need to see the plan. The recommended startup call |
| Inspect, then execute | GetSyncPlan, then ExecuteSyncPlan | Get Sync Plan, then Execute Sync Plan | You want to show the player what will change, ask for confirmation, or apply only part of the plan |
| Automatic background management | EnableAutoManagement, DisableAutoManagement | Enable Auto Management, Disable Auto Management | The 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 state | Server state | Action | Emitted only if |
|---|---|---|---|
| Not installed | Subscribed | Install | allowInstallActions |
Installed, installedFile.id differs from the id of the last entry in the subscribed mod's latestFiles | Subscribed | Update | allowUpdateActions |
| Installed and already on that file | Subscribed | No item | Nothing to do |
Installed, subscribed mod has an empty latestFiles array | Subscribed | No item | No target file can be determined |
| Installed | Not subscribed | Uninstall | allowUninstallActions. If false, the whole uninstall pass is skipped |
Two classes of installed mod are skipped in both the update and the uninstall passes.
| Skipped when | Why |
|---|---|
FInstalledMod::dynamicContent is true | Dynamic 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 true | Unmanaged mods sit in the mods directory without having been installed through the plugin, which lets mod authors play with a creation before uploading it |
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.
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).
| Value | Numeric | Meaning |
|---|---|---|
Install | 0 | Subscribed but not installed locally. Install it |
Update | 1 | Subscribed and installed, but not on the target file. Replace it |
Uninstall | 2 | Installed 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.
| Value | Numeric | Meaning |
|---|---|---|
Success | 0 | The action completed. The item's error field is a non-error FCFCoreError |
Failed | 1 | The 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.
| Field | Type | Default | Meaning |
|---|---|---|---|
action | ESubscriptionSyncAction | Install | The action the SDK intends to take |
mod | FCFCoreMod | empty | The 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.
| Field | Type | Default | Meaning |
|---|---|---|---|
modId | int64 | 0 | The mod the action applied to. A raw ID, not an FCFCoreMod: keep your plan if you need names for a results screen |
action | ESubscriptionSyncAction | Install | The action that was attempted |
status | ESubscriptionSyncItemStatus | Success | Whether that action succeeded |
error | FCFCoreError | cleared | Populated 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.
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.
| Behaviour | What it means for your integration |
|---|---|
| Actions run in parallel | All 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 time | A 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 revalidated | ExecuteSyncPlan 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.
- 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. - The first timer is one interval away. Nothing syncs immediately. If the library must be correct before the player can act, call
SyncSubscriptionsyourself as well. - Each run is a full plan build and execution, respecting the
allow*settings. - 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.
DisableAutoManagementcancels 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.
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.
- Enable management when the player enters a menu, launcher, or mod browser.
- Call
DisableAutoManagementbefore the player starts a session, loads a world, or a match begins. - Load mods from disk at your usual gate with
ICFCoreLibrary::GetInstalledMods, after management is off. - To reconcile mid-session, use
GetSyncPlanand 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.
| Setting | Type | Default | Effect |
|---|---|---|---|
automaticModManagementIntervalMs | int32 | 30000 | Milliseconds 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 |
allowInstallActions | bool | true | Permits Install items in generated sync plans |
allowUninstallActions | bool | true | Permits Uninstall items in generated sync plans |
allowUpdateActions | bool | true | Permits Update items in generated sync plans |
autoSubscribeInstalledMods | bool | false | On 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.
| Method | Endpoint | Request model | Delegate payload |
|---|---|---|---|
GetModsSubscriptions | GET /v1/users/me/mods/subscriptions | FCFCoreApiRequestPagination | TOptional<TArray<FCFCoreMod>>, TOptional<FCFCoreApiResponsePagination>, TOptional<FCFCoreApiResponseError> |
GetModsSubscriptionsIds | GET /v1/users/me/mods/subscriptions/ids | FCFCoreApiRequestPagination | TOptional<TArray<int64>>, TOptional<FCFCoreApiResponsePagination>, TOptional<FCFCoreApiResponseError> |
SubscribeMods | POST /v1/users/me/mods/subscriptions/subscribe | FSubscribeModsRequest | TOptional<FCFCoreApiResponseError> |
Request models, all USTRUCT(BlueprintType) with BlueprintReadWrite fields.
| Struct | Field | Type | Default |
|---|---|---|---|
FSubscribeModsRequest | modIds | TArray<int64> | empty |
FCFCoreApiRequestPagination | index | int32 | 0 |
FCFCoreApiRequestPagination | pageSize | int32 | 20 |
FCFCoreApiResponsePagination, returned by both listing methods. All four fields are int32 and BlueprintReadOnly.
| Field | Meaning |
|---|---|
index | Zero-based index of the first record in this page. A record offset, not a page number |
pageSize | Number of records the server used for this page |
resultCount | Number of records actually returned in this page |
totalCount | Total records available across all pages |
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.
- Build an
FCFCoreApiRequestPaginationwithindexset to0and call either method. - In the delegate, check the error optional first, then append the page payload to your accumulator.
- Compute the next index as the response
indexplus the responsepageSize. If it is greater than or equal tototalCountyou have every record, otherwise call again with it. Stop if the responsepageSizeis0or 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.
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.
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.
| Code | Raised by | Meaning and recovery |
|---|---|---|
FailedToInitialize | Every node here with an on_error pin, and any call where the interface pointer is null | Description is Not initialized. Initialize first. Disable Auto Management returns silently instead |
UserNotAuthenticated | ICFCoreSubscription::Subscribe, Unsubscribe, GetModsSubscriptions, plan building | No authenticated player and no network call made. Send them through authentication, then retry. No cfcore|Api Authorized node raises this: it returns ApiError instead |
SubscriptionSyncInProgress | SyncSubscriptions, ExecuteSyncPlan | Another execution is running. Wait for its callback, do not poll in a tight loop |
ApiError | Any call that reaches the server | The 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.
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.