Skip to main content

Premium mods and monetization

Premium mods let you sell individual pieces of user generated content inside your own game. A mod author publishes a paid mod, CurseForge holds the storefront and the entitlement record, and cfcore-sdk-ue gives your game price and discount metadata for a store surface, a purchase flow per platform, and a server signed ownership check you can trust before loading a mod off disk.

info

Premium mods must be enabled for your game. Email cfforstudios@overwolf.com, then configure the signing key in the Developer Portal at console.curseforge.com under UGC Monetization. Every call on this page needs an authenticated player, including GetModFileDownloadUrl, which requires it for premium mods specifically even though it otherwise reads like an unauthenticated call. Do not build an unauthenticated premium download path. See Authentication overview for concepts and the provider matrix, and Unreal overview for where initialization and sign in sit in the Unreal lifecycle. The shipped store surface is the mod browser, which already renders prices, discounts and the trial affordance. See The shipped store's purchase and trial contract below for what it needs from your game to work.

note

A premium call made without a valid token returns no dedicated authentication error. code is ApiError and the detail lands in apiError, so read apiError.tokenExpired and apiError.missingPrivileges to tell an auth problem from a transport problem. UserNotAuthenticated exists in the enum but is raised by the creation and subscription interfaces, never by a premium call.

Enabling premium mods and the two keys

Premium mods use two independent security mechanisms, each with its own key pair. CurseForge signs every premium response, and your client verifies that signature before trusting it. Separately, a studio can ship encrypted premium files, where the file itself is encrypted and the client decrypts it with credentials CurseForge returns.

  1. Email cfforstudios@overwolf.com to enable premium mods.
  2. Generate an RSA key pair for response signing. Set the private key in the Developer Portal under UGC Monetization, keep the public key for the client.
  3. Supply the public key at runtime with OverridePublicKey.
  4. If you ship encrypted premium files, supply the file decryption private key at runtime with OverridePrivateKey.
  5. Initialize, authenticate, then run the ownership check below before loading any premium mod.

Generate the signing pair with:

openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -pubout -in private_key.pem -out public_key.pem

FCFCoreSettingsPremiumMods on FCFCoreSettings carries both fields.

FieldTypePurpose
publicKeyPemFStringPEM public key that verifies the signature the server attaches to premium responses
privateKeyPemFStringPEM RSA private key that decrypts the guid and key pairs returned by GetPremiumModFileDetails
warning

Both are EditAnywhere, so a value set under Project Settings > Plugins > CFCore is written to DefaultGame.ini in plaintext. That works, but a player can edit that file, swap in their own public key and forge ownership responses. Setting both keys from code with OverridePublicKey and OverridePrivateKey instead keeps them out of a file the player can edit, so it is the more secure option. OverridePrivateKey is required for GetPremiumModFileDetails to decrypt at all. The SDK documents only the signing pair, so confirm the file encryption key with cfforstudios@overwolf.com.

#include <cfcore_context.h>
#include <premium/cfcore_premium_mods.h>

// Call once after Initialize, before any premium call.
// GetPublicKeyPemFromYourSecretsStore and GetPrivateKeyPemFromYourSecretsStore
// are placeholders: load the PEM strings from wherever your studio keeps
// build secrets, not from a literal in source.
bool AMyGameMode::ArmPremiumKeys() {
const FString PublicKeyPem = GetPublicKeyPemFromYourSecretsStore();
if (PublicKeyPem.IsEmpty()) {
return false; // Every premium response would be accepted unverified.
}
cfcore::ICFCorePremiumMods* Premium =
cfcore::CFCoreContext::GetInstance()->PremiumMods();
Premium->OverridePublicKey(PublicKeyPem);
Premium->OverridePrivateKey(GetPrivateKeyPemFromYourSecretsStore());
return true;
}

Blueprint: Override Public Key (cfcore|Premium Mods; InPublicKeyPem, OnSuccess, OnError) and Override Private Key (same category; InPrivateKeyPem, OnSuccess, OnError) on the CFCoreSubsystem. An override only affects later calls, so call both before the first premium call and treat OnError as a hard startup failure. Every node in cfcore|Premium Mods fires OnError with FailedToInitialize if the SDK is not initialized yet.

The premium details model

Every mod carries premiumDetails of type FPremiumDetails on FCFCoreMod. All fields are BlueprintReadOnly.

FieldTypeMeaning
isPremiumboolThe mod is a paid mod. When false, the rest of the struct is irrelevant
isFreemiumboolA premium mod installable without purchasing. After the user purchased the mod, the game unlocks features accordingly
tierPricefloatPrice as held by CurseForge. Defaults to 0
currencySymbolFStringCurrency symbol that accompanies tierPrice
platformDataFPremiumDetailsPlatformDataPlatform product info for the current player's platform
discountDataFPremiumDetailsDiscountActive discount, if any
trialDetailsFPremiumDetailsTrialTry Before You Buy config for this mod

FPremiumDetailsPlatformData has one field, productId (FString): the product id on the current player's platform. Query the platform's own APIs with it for the localised price.

FPremiumDetailsDiscountTypeMeaning
discountPricefloatThe discounted price. 0 means no discount
percentfloatEqual to (discountPrice / tierPrice) * 100
endDateFDateTimeUTC end date. After it, purchasing with the discounted product id fails. Defaults to FDateTime::MinValue()
platformDataFPremiumDetailsPlatformDataProduct id for the discounted product on the current platform
FPremiumDetailsTrialTypeMeaning
isEnabledboolWhether Try Before You Buy is enabled for this mod
allowedHoursint32How many hours the trial lasts
warning

Two traps on the discount data. It has its own product id in discountData.platformData.productId, and passing it to a platform store after endDate fails. The percent formula is the discounted price as a share of the list price, not the amount off, so a 25 percent discount computes to 75. Compute the saving yourself.

note

tierPrice and currencySymbol are CurseForge side values. Where a platform store exists, render the platform price keyed on platformData.productId and treat tierPrice as the fallback.

For a free only or paid only browsing surface, set premiumFilterType on FCFCoreSearchModsFilter.

ECFCorePremiumFilterTypeNumericBehaviour
FreeAndPremium0Default. Both free and premium mods
PremiumOnly1Premium mods only
FreeOnly2Free mods only

Blueprint: the field is BlueprintReadWrite on the FCFCoreSearchModsFilter struct you feed into Search Mods Info (cfcore|Api).

Purchase flows per platform

Two flows, no overlap. PC hands off to a browser and confirms by polling. Console asks the platform store and confirms by finalizing a transaction id. Pick by platform.

AspectPC (browser checkout)Console (platform store)
Entry pointICFCoreApiAuthorized::GeneratePremiumCheckoutUrlICFCoreApiAuthorized::InitiatePurchase
Request structFGenPremiumCheckoutUrlRequestFInitiatePurchaseRequest
Mods per callTArray<int64> modIds, many mods per basketint64 modId, exactly one
Returned payloadFString, a checkout URLFPurchaseDetails (productId, transactionId)
Where the player paysThe default browser, outside your processThe device's own store UI
Confirmation callICFCorePremiumMods::PollModPurchaseICFCoreApiAuthorized::FinalizePurchase
Confirmation requestFPollPurchaseParamsFFinalizePurchaseRequest
Platform tokenprovider plus token on the entry pointprovider plus token on both calls

Both flows take the platform's own identity token, not the SDK token. Every request struct that carries one has a provider of type ECFCoreExternalAuthProvider, defaulting to None, and a token string.

ECFCoreExternalAuthProviderNumericHow to obtain the token
None0No provider set. The default, and rejected by the server
Steam1ISteamUser::GetAuthSessionTicket, as base64
PSN2Access token from an authorization code. An environment value may be included under additional info
XBL3Access token from an XSTS token
WB4Warner Brothers
Epic5The JWT from the EOS SDK via EOS_Auth_CopyIdToken
GOG6galaxy::api::User()->RequestEncryptedAppTicket(), raw ticket bytes as base64. Needs the Encrypted App Ticket Key in your game's authentication section in the Developer Portal
note

Most providers need per provider setup in the Developer Portal before the token is accepted, so a flow that works on Steam does not automatically work on GOG.

PC purchase flow: checkout URL, browser handoff, polling

  1. Build a FGenPremiumCheckoutUrlRequest with the mod ids, the platform provider and the platform token.
  2. Call GeneratePremiumCheckoutUrl. On success you get a URL string.
  3. Start polling with PollModPurchase, then open the URL in the default browser.
  4. The player pays in the browser. Your game gets no callback from it.
  5. The delegate fires when the server reports every requested mod id as owned, when the maximum duration elapses, or when you call StopPurchasePolling.
  6. On success, install through the normal path. See mod installation methods.
FGenPremiumCheckoutUrlRequestTypeMeaning
modIdsTArray<int64>The mod ids to purchase
providerECFCoreExternalAuthProviderThe token's provider. Defaults to None
tokenFStringThe platform token, not the SDK token
trackingTMap<FString, FString>Key-value params attached to the checkout, for your own tracking or attribution
FPollPurchaseParamsTypeDefaultMeaning
modIdsTArray<int64>emptyMod ids to check. The delegate fires once all of them appear in the CheckMods response
intervalSecondsfloat5.0How often to re-check ownership while waiting
maxDurationSecondsfloat600.0Maximum total polling time before PurchasePollingTimeout

Do not hand roll a timer loop over CheckMods. PollModPurchase does that for you, designed to pair with opening a browser purchase page.

#include <api/models/gen_premium_checkout_url_request.h>
#include <premium/models/poll_purchase_params.h>

void AMyStoreActor::BuyMods(const TArray<int64>& ModIds, const FString& Token) {
FGenPremiumCheckoutUrlRequest request;
request.modIds = ModIds;
request.provider = ECFCoreExternalAuthProvider::Steam;
request.token = Token;

cfcore::CFCoreContext::GetInstance()->Api()->Authorized()
->GeneratePremiumCheckoutUrl(request,
cfcore::ICFCoreApiAuthorized::FGeneratePremiumCheckoutUrlDelegate::CreateLambda(
[ModIds](const TOptional<FString>& opt_url,
const TOptional<FCFCoreApiResponseError>& opt_err) {
if (opt_err.IsSet() || !opt_url.IsSet()) { return; }

FPollPurchaseParams params;
params.modIds = ModIds;
params.intervalSeconds = 5.0f;
params.maxDurationSeconds = 600.0f;

cfcore::CFCoreContext::GetInstance()->PremiumMods()->PollModPurchase(
params,
cfcore::ICFCorePremiumMods::FPollPurchaseDelegate::CreateLambda(
[](const TOptional<TArray<int64>>& opt_owned,
const TOptional<FCFCoreError>& opt_err) {
if (opt_err.IsSet()) {
// Switch on code: PurchasePollingTimeout (offer a manual
// re-check), PurchasePollingStopped (player cancelled),
// ApiError, FailedToVerifySignature.
return;
}
// opt_owned holds every owned mod id. Install now.
}));

FPlatformProcess::LaunchURL(**opt_url, nullptr, nullptr);
}));
}

Blueprint: Generate Premium Checkout URL (cfcore|Api Authorized; Request, OnSuccess, OnError), then Poll Mod Purchase (cfcore|Premium Mods; InParams, OnPurchased, OnError, OnPurchasePollingTimeout, OnPurchasePollingStopped). Stop Purchase Polling takes no parameters and cancels the active session.

warning

Blueprint splits the outcomes that C++ merges. In C++, timeout and cancellation arrive on the same FPollPurchaseDelegate as errors carrying PurchasePollingTimeout or PurchasePollingStopped, so you switch on FCFCoreError::code. In Blueprint they fire OnPurchasePollingTimeout and OnPurchasePollingStopped instead of OnError, so a graph that wires only OnError silently drops a timeout.

Polling behaviour that is not visible from the signature:

  • The first check happens after intervalSeconds, not immediately.
  • The timeout is checked before each scheduled check, against wall clock time from the moment PollModPurchase was called.
  • Only one polling session can be active at a time per instance. Starting a new session stops the previous one, and the previous delegate does not fire. Start a second poll for a second basket and the first UI branch never completes.
  • Success needs every id in modIds to appear in the response. A partial basket keeps polling.
  • An API or signature error during a poll ends the session immediately and propagates that error instead of retrying.
  • StopPurchasePolling is a no-op when no session is active, so call it unconditionally when your store UI closes.

Each poll is a real authenticated network round trip. Do not drive it from Tick and do not set intervalSeconds below a few seconds.

Console purchase flow: initiate, platform purchase, finalize

  1. Build a FInitiatePurchaseRequest with the single modId, the platform provider and the platform token.
  2. Call InitiatePurchase. On success you get FPurchaseDetails with a productId and a transactionId.
  3. Keep the transactionId. Run the platform's own purchase UI against productId.
  4. Build a FFinalizePurchaseRequest with that transactionId, the provider and the token.
  5. Call FinalizePurchase so the mod is attributed to the player. Until it succeeds, the platform has taken the money and CurseForge has no entitlement record.
  6. Install through the normal path.
StructFieldTypeMeaning
FInitiatePurchaseRequestmodIdint64The mod being purchased. One per call
FInitiatePurchaseRequestproviderECFCoreExternalAuthProviderThe token's provider, for example XBL
FInitiatePurchaseRequesttokenFStringThe player's platform token
FPurchaseDetailsproductIdFStringProduct id to purchase with on the device. Needs Developer Portal setup, so open that request with cfforstudios@overwolf.com
FPurchaseDetailstransactionIdFStringPass to FinalizePurchase after the device purchase completes
FFinalizePurchaseRequesttransactionIdFStringThe transaction id from the initiate call
FFinalizePurchaseRequestproviderECFCoreExternalAuthProviderThe token's provider
FFinalizePurchaseRequesttokenFStringThe player's platform token
FFinalizePurchaseRequesttrackingTMap<FString, FString>Key-value params attached to the finalize call, for your own tracking or attribution
FInitiatePurchaseRequest initiate;
initiate.modId = ModId;
initiate.provider = ECFCoreExternalAuthProvider::XBL;
initiate.token = Token;

cfcore::CFCoreContext::GetInstance()->Api()->Authorized()->InitiatePurchase(
initiate,
cfcore::ICFCoreApiAuthorized::FInitiatePurchaseDelegate::CreateLambda(
[Token](const TOptional<FPurchaseDetails>& opt_details,
const TOptional<FCFCoreApiResponseError>& opt_err) {
if (opt_err.IsSet() || !opt_details.IsSet()) { return; }

// Persist transactionId, run the platform store purchase for
// opt_details->productId, then:
FFinalizePurchaseRequest finalize;
finalize.transactionId = opt_details->transactionId;
finalize.provider = ECFCoreExternalAuthProvider::XBL;
finalize.token = Token;

cfcore::CFCoreContext::GetInstance()->Api()->Authorized()->FinalizePurchase(
finalize,
cfcore::ICFCoreApiAuthorized::FFinalizePurchaseDelegate::CreateLambda(
[](const TOptional<FCFCoreApiResponseError>& opt_finalize_err) {
// On success the mod is attributed to the player.
}));
}));

Blueprint: Initiate Purchase (cfcore|Api Authorized; Request, OnSuccess, OnError) returns an FPurchaseDetails. Finalize Purchase (same category and pins) has a success pin with no payload. Both request structs are BlueprintType with BlueprintReadWrite fields, so build them with a Make node.

tip

Persist transactionId to disk before the handoff. The SDK retries FinalizePurchase automatically on a transient server error, but only while your process is still running. A crash or restart before it succeeds loses that state entirely, so the transaction id is the only handle left to attribute the purchase. Certification requirements for purchase and consent flows are in Xbox and PlayStation compliance.

The shipped store's purchase and trial contract

The mod browser ships a store menu, price columns, sale and premium tags, and a code redemption screen, but it does not execute a purchase or know how your game takes money. It exposes a contract your game fulfils, the same shape as the purchase flows above, wired through UCFCoreUISubsystem instead of called directly:

  1. Bind UIPurchaseModDelegate (fires with a single mod id) and UIPurchaseModsDelegate (fires with an array of mod ids) to run the purchase flow described above for the platform the build is running on.
  2. After a purchase or restore completes, populate UserPurchasedMods (TArray<int64>, Blueprint name PurchasedMods) from GetPremiumModsV2().ownedMods. If this is never written, every premium mod renders as unowned across the browser, since the shipped mod tile, mod page and store widgets all read it.
  3. For Try Before You Buy, populate TrialStatus (TArray<FUserTrialData>, Blueprint name TrialStatus) from the same GetPremiumModsV2 call, or call AddModToTrialListLocally(int64 modId, bool isExpired, FString expiresAt) per mod as trials start.

Blueprint: the two purchase delegates and the three properties above are all on CFCoreUISubsystem, category cfcore_ui. UIPurchaseModDelegate and UIPurchaseModsDelegate are BlueprintReadWrite, so bind them with Assign or Bind Event to the same way you would any other assignable delegate.

Ownership and trial checks at session start

Loading a paid mod without an ownership check is the failure mode that costs money. Use the signature checking wrappers on ICFCorePremiumMods, not the raw ICFCoreApiAuthorized equivalents.

All three are on ICFCorePremiumMods.

C++BlueprintReturnsUse for
GetPremiumModsV2Get My Premium Mods V2FOwnedPremiumModsSession start gating. The only call that returns trial state
CheckModsPremium Mods CheckTArray<int64>, the owned subsetA known basket. The primitive PollModPurchase drives internally
CheckDlcPremium Mods Check DlcTArray<int64> of owned mod idsPremium mods sold as platform DLC
FOwnedPremiumModsTypeMeaning
ownedModsTArray<int64>Mod ids the player purchased. Always loadable
trialModsTArray<FUserTrialData>Mods the player is currently trialling
FUserTrialDataTypeMeaning
modIdint64The mod being trialled
isExpiredboolSet server side. The authoritative expiry signal
expiresAtFStringISO 8601 UTC string, for example 2025-01-01T00:00:00Z. Display only
  1. At session start, after authentication, call GetPremiumModsV2 once.
  2. For each mod you are about to load, skip the ownership check when the mod is not premium, is freemium, or the player holds an active trial for it.
  3. Otherwise load only if the mod id is in ownedMods. If not, block the load and prompt for purchase.
  4. Enforce the gate at session start, not mid session.

Three conditions exempt a mod from the ownership check: FPremiumDetails::isPremium is false, isFreemium is true, or FOwnedPremiumMods::trialMods holds an entry for that mod id with isExpired false. Write this check once and reuse it everywhere you gate premium content, since the SDK's own dynamic content enforcement applies the same three conditions.

void AMyGameMode::GateModsAtSessionStart() {
cfcore::CFCoreContext::GetInstance()->PremiumMods()->GetPremiumModsV2(
cfcore::ICFCorePremiumMods::FGetPremiumModsV2Delegate::CreateLambda(
[this](const TOptional<FOwnedPremiumMods>& opt_owned,
const TOptional<FCFCoreError>& opt_err) {
if (opt_err.IsSet() || !opt_owned.IsSet()) {
return; // Includes FailedToVerifySignature. Load no premium mods.
}
const FOwnedPremiumMods& Owned = opt_owned.GetValue();
for (const FInstalledMod& Installed : GetInstalledModsToLoad()) {
const int64 ModId = Installed.details.id;
const FPremiumDetails& Details = Installed.details.premiumDetails;

const bool bHasActiveTrial = Owned.trialMods.ContainsByPredicate(
[ModId](const FUserTrialData& Trial) {
return Trial.modId == ModId && !Trial.isExpired;
});

if (!Details.isPremium || Details.isFreemium || bHasActiveTrial ||
Owned.ownedMods.Contains(ModId)) {
LoadMod(Installed);
continue;
}
BlockAndPromptPurchase(ModId);
}
}));
}

Blueprint: Get My Premium Mods V2 (cfcore|Premium Mods; OnSuccess, OnError) returns FOwnedPremiumMods with ownedMods and trialMods as BlueprintReadOnly arrays. Premium Mods Check takes InModIds, OnSuccess, OnError. Break each mod's premiumDetails for isPremium and isFreemium, and loop trialMods for an unexpired entry matching the mod id, to build the same gate.

note

These are live authenticated requests, not cached local lookups, so the player must be online. Decide your offline session policy before you ship.

note

The join a server path checks ownership for you. AssureClientModsUpdated (Blueprint: Assure Client Mods Updated, cfcore|Library|ClientServer; ServerFileIds, OnProgress, OnUpdated, OnError) runs the validation itself, so no second manual check is needed there. It submits every server mod id whose isFreemium is false regardless of the client side isPremium flag, because the server is the authority on which mods are paid. An unowned mod surfaces as ModsNotOwnedByUser.

warning

That error carries no mod ids. It is a pass or fail gate, so it cannot drive a purchase screen that names the mods or shows a price. If your join UI needs that, call CheckMods yourself with the server's mod ids and diff the result. See Multiplayer and servers for the sequence.

Try Before You Buy

Try Before You Buy lets a player use a paid mod for a fixed window before purchasing. Each player gets one trial per mod, and the offer comes from the mod's own metadata, not a global setting.

info

Email cfforstudios@overwolf.com and the feature is enabled for your game as TryBeforeYouBuy in the Game Moderation section of the Developer Portal at console.curseforge.com. No code changes are needed to turn it on.

  1. Read premiumDetails.trialDetails. Show a start trial affordance only when isEnabled is true, and use allowedHours in the copy, for example "Try free for 24 hours".
  2. The player starts the trial from the mod page. The mod browser handles that interaction.
  3. The mod installs, the countdown begins, and the player has full access.
  4. When the trial expires the current session continues uninterrupted.
  5. On the next session start, GetPremiumModsV2 no longer returns the mod in trialMods, or returns it with isExpired true. Your gate disables it and prompts for purchase.
warning

Do not trust the local clock. expiresAt exists so you can render a countdown. Do not compare it against the local clock, because players can change it. isExpired is set server side and is the only field that may gate access. A gate written as FDateTime::UtcNow() > ParsedExpiresAt is defeated by changing the system clock.

Signature verification

ICFCorePremiumMods is the signature verifying layer over the raw authorized API. Five methods wrap a raw call and add verification. OverridePublicKey, OverridePrivateKey, PollModPurchase and StopPurchasePolling wrap nothing.

ICFCorePremiumMods methodWrapsVerification added
GetPremiumModsV2ICFCoreApiAuthorized::GetMyPremiumModsV2Signature check over the raw response body
CheckModsICFCoreApiAuthorized::PremiumModsCheckSignature check over a payload rebuilt from the response model
CheckDlcICFCoreApiAuthorized::CheckDlcSignature check over a payload rebuilt from the response model
GetModFileDownloadUrlICFCoreApi::GetModFileDownloadUrlSignature check, the protected version of the download URL call
GetPremiumModFileDetailsICFCoreApiAuthorized::GetPremiumModFileDetailsSignature check over the raw response body, then RSA decryption of the guid and key pairs

Verification uses RSA-PSS with SHA-256 over the response payload with the signature field removed. Failure returns FailedToVerifySignature and no data. A missing signature while a public key is configured counts as a failure, not as an unsigned response.

danger

If neither publicKeyPem nor OverridePublicKey is set, verification returns success without checking anything, and every premium response is accepted. Assert your public key is non-empty at startup in shipping builds so this cannot ship silently. The runtime override wins over the settings value, which is used only when the override is empty. If verification starts failing after a Developer Portal change, check that the UGC Monetization private key still matches the public key in the build.

To verify in your own code, call the raw ICFCoreApiAuthorized methods instead. Their delegates carry the base64 signature as an extra parameter, and GetMyPremiumModsV2 and GetPremiumModFileDetails also carry the raw response body the signature was computed over. Use that raw body, not a re-serialized model, because re-serializing a USTRUCT can change field name casing. Every delegate on ICFCorePremiumMods takes exactly two parameters, a TOptional model and a TOptional<FCFCoreError>.

Blueprint: the raw calls have no nodes. Blueprint gets Get My Premium Mods V2, Premium Mods Check, Premium Mods Check Dlc and Get Premium Mod File Details, all routed through the verifying layer, so a Blueprint only integration gets verification on all of them once a key is set.

Protected premium file downloads

Premium files add two mechanics on top of a normal download: the URL is signed, and file contents can be encrypted with per file credentials only your game can decrypt.

ICFCorePremiumMods::GetModFileDownloadUrl(int64 mod_id, int64 file_id, FGetModFileDownloadUrlDelegate) returns FModFileDownloadUrl after verifying the signature: url (FString) and securedParams (FString), the secured params that accompany it.

note

This is C++ only, with no Blueprint node. A Blueprint only integration cannot fetch a signature verified download URL.

For encrypted files the server returns an RSA encrypted guid and key per file. GetPremiumModFileDetails verifies the signature, decrypts both with your private key, and hands back plaintext credentials to pass to the engine's pak mounting API. The request struct FGetPremiumModFileDetailsRequest has one field, fileIds (TArray<int64>).

StructFieldTypeMeaning
FPremiumModFileDetailsfilesTArray<FPremiumModFileData>One entry per requested file
FPremiumModFileDatafileIdint64The file these credentials belong to
FPremiumModFileDatacredentialsFPremiumModFileCredentialsDecrypted credentials. Empty when the file is not encrypted
FPremiumModFileCredentialsguidFGuidDecrypted and parsed guid
FPremiumModFileCredentialskeyFStringDecrypted key

The pre decryption shape from the raw API mirrors it, and you only need it if you decrypt yourself: FPremiumModFileEncryptedDetails::files is a TArray<FPremiumModFileEncryptedData>, each with fileId (int64) and encryptionData (FPremiumModFileEncryptedCredentials), whose guid and key are both RSA encrypted base64 FString values.

#include <api/models/get_premium_mod_file_details_request.h>

FGetPremiumModFileDetailsRequest request;
request.fileIds = FileIds;

cfcore::CFCoreContext::GetInstance()->PremiumMods()->GetPremiumModFileDetails(
request,
cfcore::ICFCorePremiumMods::FGetPremiumModFileDetailsDelegate::CreateLambda(
[](const TOptional<FPremiumModFileDetails>& opt_details,
const TOptional<FCFCoreError>& opt_err) {
if (opt_err.IsSet() || !opt_details.IsSet()) {
return; // FailedToDecrypt means the private key is missing or wrong.
}
for (const FPremiumModFileData& File : opt_details->files) {
if (!File.credentials.guid.IsValid()) {
continue; // Not an encrypted file. Mount normally.
}
// Pass File.credentials.guid and File.credentials.key to the
// engine's pak mounting API.
}
}));

Blueprint: Get Premium Mod File Details (cfcore|Premium Mods; InRequest, OnSuccess, OnError). OnSuccess carries an FPremiumModFileDetails.

Decryption behaviour that is not visible from the signature:

  • A private key is required for any entry that carries credentials. Without one the whole call returns FailedToDecrypt.
  • An entry with neither a guid nor a key is valid and is skipped before the private key is consulted, so a request made entirely of unencrypted files succeeds with no private key set.
  • An entry with only one of the two present is a hard failure for the whole batch, as is a decrypted guid that does not parse as an FGuid.
  • Failure is all or nothing, so request only the file ids you intend to mount.

Redeem codes and entitlement campaigns

CurseForge can also grant premium content through codes and through platform DLC entitlements. Both end with the player owning a premium mod, so your ownership gate does not need to know which route granted it. Campaigns are configured in the Developer Portal. The mod browser ships a redeem-code screen that already calls EntitlementsRedeemCode, so a studio using that UI does not need to build the entry form itself.

PathC++ and BlueprintInputOutput
Redeem a codeEntitlementsRedeemCode, Entitlements Redeem CodeFEntitlementsRedeemCodeRequestFEntitlementCampaign
Check platform DLCCheckDlc, Premium Mods Check DlcTArray<FString> of entitlement idsTArray<int64> of owned mod ids

EntitlementsRedeemCode is on ICFCoreApiAuthorized, and its request carries one field, code. CheckDlc is on ICFCorePremiumMods, covered in full above.

FEntitlementCampaignTypeMeaning
idint64Campaign id
gameIdint64The game the campaign belongs to
nameFStringCampaign name
createdAtFDateTimeCreation date
expiresAtFDateTimeExpiry date
typeECFCoreEntitlmentCampaignTypeWhat it grants. Defaults to Unknown
isDeletedboolWhether the campaign has been deleted
codeLengthint32Code length for this campaign
extraDataFStringJSON data. For a Mod campaign this contains the mod id
ECFCoreEntitlmentCampaignTypeNumericMeaning
Unknown0The default. Not a grant, so treat it as unhandled
Mod1Grants a mod, for example a premium mod. extraData holds the mod id
Feature2A feature entitlement rather than a specific mod. What it grants is not documented
warning

extraData is a raw JSON string, not a parsed struct. Parse it yourself for the mod id, then re-run your ownership gate. Branch on the known type values and fail closed on Unknown.

FEntitlementsRedeemCodeRequest request;
request.code = Code;

cfcore::CFCoreContext::GetInstance()->Api()->Authorized()->EntitlementsRedeemCode(
request,
cfcore::ICFCoreApiAuthorized::FEntitlementsRedeemCodeDelegate::CreateLambda(
[](const TOptional<FEntitlementCampaign>& opt_campaign,
const TOptional<FCFCoreApiResponseError>& opt_err) {
if (opt_err.IsSet() || !opt_campaign.IsSet()) { return; }
if (opt_campaign->type == ECFCoreEntitlmentCampaignType::Mod) {
// extraData is JSON holding the mod id. Parse it, re-run the gate.
}
}));

Blueprint: Entitlements Redeem Code (cfcore|Api Authorized; InRequest, OnSuccess, OnError) and Premium Mods Check Dlc (cfcore|Premium Mods; InEntitlementIds, OnSuccess, OnError).

info

Reach cfforstudios@overwolf.com before wiring a DLC based ownership path. The check is not read only: it may result in the player gaining ownership of premium mods, because it reconciles external entitlements into CurseForge ownership.

Premium dynamic content enforcement

Dynamic content is a mod, usually a cosmetic, that the game downloads automatically so every player in a session can see it without non owning players being able to use it. The assets exist on disk, the mod is not treated as installed, and only the owner gets the functionality.

FieldWhereTypeMeaning
dynamicContentFInstallModAdditionalParamsboolInstall as dynamic content: on disk, not owned or requested
originFInstallModAdditionalParamsEModInstallOriginInstall origin. Overwritten by the SDK, see below
dynamicContentFInstalledModboolThe player has the mod on disk but did not request or own it
dynamicContentCategoryIdsFCFCoreSettingsTSet<int64>Category ids that support dynamic content
note

When dynamicContent is true on the install request, the SDK overwrites whatever origin you set with EModInstallOrigin::DynamicDownload and adds its own dynamic content tracking key. dynamicContentCategoryIds is used mainly to check ownership of premium dynamic content.

  1. Add the category ids that carry your cosmetics to dynamicContentCategoryIds under Project Settings > Plugins > CFCore.
  2. When a player joins a session where another player owns a cosmetic, install that mod with dynamicContent set to true.
  3. The mod appears in the library with FInstalledMod::dynamicContent true. Render it on the owning player, do not grant it to the local player.
  4. If the local player later purchases or installs it, the regular install functions flip the flag to false. No special call is needed.
warning

If you host premium dynamic content, dynamicContentCategoryIds is not optional. The SDK only runs the premium ownership check on dynamic content for mods in those categories, so an empty set means a player who edits their local library metadata can promote dynamic content into owned content on their device.

note

The check is automatic and you do not call it. The SDK runs it as part of the installed mods validation pass and on the client server path when a player joins a server.

StepBehaviour
Which mods are scannedInstalled mods whose FInstalledMod::dynamicContent is false and at least one of whose category ids is in dynamicContentCategoryIds
Which are then droppedAny mod that is not premium, is freemium, or has an active trial
Ownership sourceGetPremiumModsV2, so the check inherits signature verification
Fewer mod details returned than installedFails with MissingModsDetails
An unowned mod is foundIts dynamicContent flag is reset to true, so it reverts to view only. The mod is not uninstalled or deleted, and the call completes with ModsNotOwnedByUser
FInstallModAdditionalParams params;
params.dynamicContent = true;
// The SDK forces origin to EModInstallOrigin::DynamicDownload and adds its own
// dynamic content tracking key, so do not rely on overriding either here.
// Install through the library with these additional params.

Blueprint: Install Mod Extended (cfcore|Library; InMod, InFile, InAdditionalParams, OnProgress, OnInstalled, OnError), with dynamicContent set on the FInstallModAdditionalParams struct you feed into InAdditionalParams. The plain Install Mod node does not expose the flag. dynamicContentCategoryIds is a project setting, not a node.

Error codes for premium flows

Every premium call reports failure through FCFCoreError: isError (bool), code (ECFCoreErrorCodes), apiError (FCFCoreApiResponseError) and description (FString). Branch on code, use description for logs only.

ECFCoreErrorCodesRaised byMeaning and recovery
ApiErrorAny premium callTransport or server side failure, including an expired or missing auth token. Inspect apiError
FailedToVerifySignatureGetPremiumModsV2, CheckMods, CheckDlc, GetModFileDownloadUrl, GetPremiumModFileDetailsThe signature did not verify, or was missing while a public key was configured. Load no premium content. Check that the Developer Portal private key matches the shipped public key
FailedToDecryptGetPremiumModFileDetailsCredentials could not be decrypted. Usually a missing or wrong private key, partial credentials, or a decrypted guid that is not a valid GUID
PurchasePollingTimeoutPollModPurchasemaxDurationSeconds elapsed with no ownership detected. Offer a manual re-check, not a second checkout
PurchasePollingStoppedPollModPurchase after StopPurchasePollingCancelled deliberately. Not a player facing error
ModsNotOwnedByUserThe client server join path including AssureClientModsUpdated, and the dynamic content checkOne or more mods are not owned. The player must purchase them to use them
MissingModsDetailsThe dynamic content checkThe server returned details for fewer mods than are installed, so ownership could not be established for all of them
FailedToInitializeAny Blueprint node in cfcore|Premium Mods or cfcore|Api AuthorizedThe SDK was not initialized before the node ran

When code is ApiError, the cause is in the bool flags on apiError: tokenExpired (re-authenticate, then retry), missingPrivileges (valid token, not permitted), badRequest (empty token, or a provider left at None), serverUnreachable, resourceExpired (a discount or campaign past its end date), entityNotFound, cancelled, failedToParseServerResponse. errorCode (int32) is the server side code, description (FString) is for logs.

UserNotAuthenticated and MissingParameter read as though they belong here but are raised by the creation and subscription interfaces. Do not branch on them in a premium flow.

Getting help

Enabling premium mods, the UGC Monetization signing key, the file encryption key, TryBeforeYouBuy, per provider authentication setup, console store product ids, entitlement campaigns and the DLC check all need Developer Portal configuration by the CurseForge team. Open those requests with cfforstudios@overwolf.com and manage keys, campaigns and authentication providers yourself in the Developer Portal at console.curseforge.com. For game id and API key setup see setting up your game.