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.
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.
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.
- Email cfforstudios@overwolf.com to enable premium mods.
- 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.
- Supply the public key at runtime with
OverridePublicKey. - If you ship encrypted premium files, supply the file decryption private key at runtime with
OverridePrivateKey. - 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.
| Field | Type | Purpose |
|---|---|---|
publicKeyPem | FString | PEM public key that verifies the signature the server attaches to premium responses |
privateKeyPem | FString | PEM RSA private key that decrypts the guid and key pairs returned by GetPremiumModFileDetails |
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.
| Field | Type | Meaning |
|---|---|---|
isPremium | bool | The mod is a paid mod. When false, the rest of the struct is irrelevant |
isFreemium | bool | A premium mod installable without purchasing. After the user purchased the mod, the game unlocks features accordingly |
tierPrice | float | Price as held by CurseForge. Defaults to 0 |
currencySymbol | FString | Currency symbol that accompanies tierPrice |
platformData | FPremiumDetailsPlatformData | Platform product info for the current player's platform |
discountData | FPremiumDetailsDiscount | Active discount, if any |
trialDetails | FPremiumDetailsTrial | Try 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.
FPremiumDetailsDiscount | Type | Meaning |
|---|---|---|
discountPrice | float | The discounted price. 0 means no discount |
percent | float | Equal to (discountPrice / tierPrice) * 100 |
endDate | FDateTime | UTC end date. After it, purchasing with the discounted product id fails. Defaults to FDateTime::MinValue() |
platformData | FPremiumDetailsPlatformData | Product id for the discounted product on the current platform |
FPremiumDetailsTrial | Type | Meaning |
|---|---|---|
isEnabled | bool | Whether Try Before You Buy is enabled for this mod |
allowedHours | int32 | How many hours the trial lasts |
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.
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.
ECFCorePremiumFilterType | Numeric | Behaviour |
|---|---|---|
FreeAndPremium | 0 | Default. Both free and premium mods |
PremiumOnly | 1 | Premium mods only |
FreeOnly | 2 | Free 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.
| Aspect | PC (browser checkout) | Console (platform store) |
|---|---|---|
| Entry point | ICFCoreApiAuthorized::GeneratePremiumCheckoutUrl | ICFCoreApiAuthorized::InitiatePurchase |
| Request struct | FGenPremiumCheckoutUrlRequest | FInitiatePurchaseRequest |
| Mods per call | TArray<int64> modIds, many mods per basket | int64 modId, exactly one |
| Returned payload | FString, a checkout URL | FPurchaseDetails (productId, transactionId) |
| Where the player pays | The default browser, outside your process | The device's own store UI |
| Confirmation call | ICFCorePremiumMods::PollModPurchase | ICFCoreApiAuthorized::FinalizePurchase |
| Confirmation request | FPollPurchaseParams | FFinalizePurchaseRequest |
| Platform token | provider plus token on the entry point | provider 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.
ECFCoreExternalAuthProvider | Numeric | How to obtain the token |
|---|---|---|
None | 0 | No provider set. The default, and rejected by the server |
Steam | 1 | ISteamUser::GetAuthSessionTicket, as base64 |
PSN | 2 | Access token from an authorization code. An environment value may be included under additional info |
XBL | 3 | Access token from an XSTS token |
WB | 4 | Warner Brothers |
Epic | 5 | The JWT from the EOS SDK via EOS_Auth_CopyIdToken |
GOG | 6 | galaxy::api::User()->RequestEncryptedAppTicket(), raw ticket bytes as base64. Needs the Encrypted App Ticket Key in your game's authentication section in the Developer Portal |
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
- Build a
FGenPremiumCheckoutUrlRequestwith the mod ids, the platformproviderand the platformtoken. - Call
GeneratePremiumCheckoutUrl. On success you get a URL string. - Start polling with
PollModPurchase, then open the URL in the default browser. - The player pays in the browser. Your game gets no callback from it.
- The delegate fires when the server reports every requested mod id as owned, when the maximum duration elapses, or when you call
StopPurchasePolling. - On success, install through the normal path. See mod installation methods.
FGenPremiumCheckoutUrlRequest | Type | Meaning |
|---|---|---|
modIds | TArray<int64> | The mod ids to purchase |
provider | ECFCoreExternalAuthProvider | The token's provider. Defaults to None |
token | FString | The platform token, not the SDK token |
tracking | TMap<FString, FString> | Key-value params attached to the checkout, for your own tracking or attribution |
FPollPurchaseParams | Type | Default | Meaning |
|---|---|---|---|
modIds | TArray<int64> | empty | Mod ids to check. The delegate fires once all of them appear in the CheckMods response |
intervalSeconds | float | 5.0 | How often to re-check ownership while waiting |
maxDurationSeconds | float | 600.0 | Maximum 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.
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
PollModPurchasewas 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
modIdsto 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.
StopPurchasePollingis 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
- Build a
FInitiatePurchaseRequestwith the singlemodId, the platformproviderand the platformtoken. - Call
InitiatePurchase. On success you getFPurchaseDetailswith aproductIdand atransactionId. - Keep the
transactionId. Run the platform's own purchase UI againstproductId. - Build a
FFinalizePurchaseRequestwith thattransactionId, the provider and the token. - Call
FinalizePurchaseso the mod is attributed to the player. Until it succeeds, the platform has taken the money and CurseForge has no entitlement record. - Install through the normal path.
| Struct | Field | Type | Meaning |
|---|---|---|---|
FInitiatePurchaseRequest | modId | int64 | The mod being purchased. One per call |
FInitiatePurchaseRequest | provider | ECFCoreExternalAuthProvider | The token's provider, for example XBL |
FInitiatePurchaseRequest | token | FString | The player's platform token |
FPurchaseDetails | productId | FString | Product id to purchase with on the device. Needs Developer Portal setup, so open that request with cfforstudios@overwolf.com |
FPurchaseDetails | transactionId | FString | Pass to FinalizePurchase after the device purchase completes |
FFinalizePurchaseRequest | transactionId | FString | The transaction id from the initiate call |
FFinalizePurchaseRequest | provider | ECFCoreExternalAuthProvider | The token's provider |
FFinalizePurchaseRequest | token | FString | The player's platform token |
FFinalizePurchaseRequest | tracking | TMap<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.
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:
- Bind
UIPurchaseModDelegate(fires with a single mod id) andUIPurchaseModsDelegate(fires with an array of mod ids) to run the purchase flow described above for the platform the build is running on. - After a purchase or restore completes, populate
UserPurchasedMods(TArray<int64>, Blueprint name PurchasedMods) fromGetPremiumModsV2().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. - For Try Before You Buy, populate
TrialStatus(TArray<FUserTrialData>, Blueprint name TrialStatus) from the sameGetPremiumModsV2call, or callAddModToTrialListLocally(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++ | Blueprint | Returns | Use for |
|---|---|---|---|
GetPremiumModsV2 | Get My Premium Mods V2 | FOwnedPremiumMods | Session start gating. The only call that returns trial state |
CheckMods | Premium Mods Check | TArray<int64>, the owned subset | A known basket. The primitive PollModPurchase drives internally |
CheckDlc | Premium Mods Check Dlc | TArray<int64> of owned mod ids | Premium mods sold as platform DLC |
FOwnedPremiumMods | Type | Meaning |
|---|---|---|
ownedMods | TArray<int64> | Mod ids the player purchased. Always loadable |
trialMods | TArray<FUserTrialData> | Mods the player is currently trialling |
FUserTrialData | Type | Meaning |
|---|---|---|
modId | int64 | The mod being trialled |
isExpired | bool | Set server side. The authoritative expiry signal |
expiresAt | FString | ISO 8601 UTC string, for example 2025-01-01T00:00:00Z. Display only |
- At session start, after authentication, call
GetPremiumModsV2once. - 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.
- Otherwise load only if the mod id is in
ownedMods. If not, block the load and prompt for purchase. - 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.
These are live authenticated requests, not cached local lookups, so the player must be online. Decide your offline session policy before you ship.
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.
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.
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.
- Read
premiumDetails.trialDetails. Show a start trial affordance only whenisEnabledis true, and useallowedHoursin the copy, for example "Try free for 24 hours". - The player starts the trial from the mod page. The mod browser handles that interaction.
- The mod installs, the countdown begins, and the player has full access.
- When the trial expires the current session continues uninterrupted.
- On the next session start,
GetPremiumModsV2no longer returns the mod intrialMods, or returns it withisExpiredtrue. Your gate disables it and prompts for purchase.
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 method | Wraps | Verification added |
|---|---|---|
GetPremiumModsV2 | ICFCoreApiAuthorized::GetMyPremiumModsV2 | Signature check over the raw response body |
CheckMods | ICFCoreApiAuthorized::PremiumModsCheck | Signature check over a payload rebuilt from the response model |
CheckDlc | ICFCoreApiAuthorized::CheckDlc | Signature check over a payload rebuilt from the response model |
GetModFileDownloadUrl | ICFCoreApi::GetModFileDownloadUrl | Signature check, the protected version of the download URL call |
GetPremiumModFileDetails | ICFCoreApiAuthorized::GetPremiumModFileDetails | Signature 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.
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.
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>).
| Struct | Field | Type | Meaning |
|---|---|---|---|
FPremiumModFileDetails | files | TArray<FPremiumModFileData> | One entry per requested file |
FPremiumModFileData | fileId | int64 | The file these credentials belong to |
FPremiumModFileData | credentials | FPremiumModFileCredentials | Decrypted credentials. Empty when the file is not encrypted |
FPremiumModFileCredentials | guid | FGuid | Decrypted and parsed guid |
FPremiumModFileCredentials | key | FString | Decrypted 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.
| Path | C++ and Blueprint | Input | Output |
|---|---|---|---|
| Redeem a code | EntitlementsRedeemCode, Entitlements Redeem Code | FEntitlementsRedeemCodeRequest | FEntitlementCampaign |
| Check platform DLC | CheckDlc, Premium Mods Check Dlc | TArray<FString> of entitlement ids | TArray<int64> of owned mod ids |
EntitlementsRedeemCode is on ICFCoreApiAuthorized, and its request carries one field, code. CheckDlc is on ICFCorePremiumMods, covered in full above.
FEntitlementCampaign | Type | Meaning |
|---|---|---|
id | int64 | Campaign id |
gameId | int64 | The game the campaign belongs to |
name | FString | Campaign name |
createdAt | FDateTime | Creation date |
expiresAt | FDateTime | Expiry date |
type | ECFCoreEntitlmentCampaignType | What it grants. Defaults to Unknown |
isDeleted | bool | Whether the campaign has been deleted |
codeLength | int32 | Code length for this campaign |
extraData | FString | JSON data. For a Mod campaign this contains the mod id |
ECFCoreEntitlmentCampaignType | Numeric | Meaning |
|---|---|---|
Unknown | 0 | The default. Not a grant, so treat it as unhandled |
Mod | 1 | Grants a mod, for example a premium mod. extraData holds the mod id |
Feature | 2 | A feature entitlement rather than a specific mod. What it grants is not documented |
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).
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.
| Field | Where | Type | Meaning |
|---|---|---|---|
dynamicContent | FInstallModAdditionalParams | bool | Install as dynamic content: on disk, not owned or requested |
origin | FInstallModAdditionalParams | EModInstallOrigin | Install origin. Overwritten by the SDK, see below |
dynamicContent | FInstalledMod | bool | The player has the mod on disk but did not request or own it |
dynamicContentCategoryIds | FCFCoreSettings | TSet<int64> | Category ids that support dynamic content |
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.
- Add the category ids that carry your cosmetics to
dynamicContentCategoryIdsunder Project Settings > Plugins > CFCore. - When a player joins a session where another player owns a cosmetic, install that mod with
dynamicContentset to true. - The mod appears in the library with
FInstalledMod::dynamicContenttrue. Render it on the owning player, do not grant it to the local player. - If the local player later purchases or installs it, the regular install functions flip the flag to false. No special call is needed.
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.
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.
| Step | Behaviour |
|---|---|
| Which mods are scanned | Installed mods whose FInstalledMod::dynamicContent is false and at least one of whose category ids is in dynamicContentCategoryIds |
| Which are then dropped | Any mod that is not premium, is freemium, or has an active trial |
| Ownership source | GetPremiumModsV2, so the check inherits signature verification |
| Fewer mod details returned than installed | Fails with MissingModsDetails |
| An unowned mod is found | Its 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.
ECFCoreErrorCodes | Raised by | Meaning and recovery |
|---|---|---|
ApiError | Any premium call | Transport or server side failure, including an expired or missing auth token. Inspect apiError |
FailedToVerifySignature | GetPremiumModsV2, CheckMods, CheckDlc, GetModFileDownloadUrl, GetPremiumModFileDetails | The 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 |
FailedToDecrypt | GetPremiumModFileDetails | Credentials could not be decrypted. Usually a missing or wrong private key, partial credentials, or a decrypted guid that is not a valid GUID |
PurchasePollingTimeout | PollModPurchase | maxDurationSeconds elapsed with no ownership detected. Offer a manual re-check, not a second checkout |
PurchasePollingStopped | PollModPurchase after StopPurchasePolling | Cancelled deliberately. Not a player facing error |
ModsNotOwnedByUser | The client server join path including AssureClientModsUpdated, and the dynamic content check | One or more mods are not owned. The player must purchase them to use them |
MissingModsDetails | The dynamic content check | The server returned details for fewer mods than are installed, so ownership could not be established for all of them |
FailedToInitialize | Any Blueprint node in cfcore|Premium Mods or cfcore|Api Authorized | The 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.