Paid Content lets players unlock part of your game with a one-time in-game purchase. You define the locked files, price, and paywall appearance in the Developer Portal (see Monetization); the SDK lets your game check ownership and open the paywall, keyed by the content identifier you set on each offer.
isEntitled and getEntitlements are UI hints; use them to decide what to show, not as the lock itself. Wavedash re-checks ownership when it serves the paid files, so locked content stays protected even if a client-side check is bypassed — see Fetching the paid files.
Checking entitlement
isEntitled returns whether the player already owns a content identifier; use it to unlock content on load or to decide whether to show the paywall.
func check_full_version():
var result = await WavedashSDK.is_entitled("full-version")
if result.success and result.data:
await fetch_paid_assets()
unlock_full_version()
bool owned = await Wavedash.SDK.IsEntitled("full-version");
if (owned)
{
await FetchPaidAssets();
UnlockFullVersion();
}
const result = await Wavedash.isEntitled("full-version");
if (result.success && result.data) {
await fetchPaidAssets();
unlockFullVersion();
}
Listing everything a player owns
getEntitlements returns every content identifier the player owns for your game, so you can gate several items in one call.
func load_entitlements():
var result = await WavedashSDK.get_entitlements()
if result.success:
for id in result.data:
print("Owns: ", id)
List<string> owned = await Wavedash.SDK.GetEntitlements();
foreach (var id in owned)
Debug.Log($"Owns: {id}");
const result = await Wavedash.getEntitlements();
if (result.success) {
for (const id of result.data) console.log("Owns:", id);
}
Opening the paywall
triggerPaywall opens the Wavedash-rendered checkout for a content identifier. It resolves immediately if the player already owns it; otherwise it opens the modal and resolves with whether the purchase completed.
func on_unlock_pressed():
var result = await WavedashSDK.trigger_paywall("full-version")
if result.success and result.data:
await fetch_paid_assets()
unlock_full_version()
bool purchased = await Wavedash.SDK.TriggerPaywall("full-version");
if (purchased)
{
await FetchPaidAssets();
UnlockFullVersion();
}
const result = await Wavedash.triggerPaywall("full-version");
if (result.success && result.data) {
await fetchPaidAssets();
unlockFullVersion();
}
After a successful purchase, ownership refreshes automatically, so isEntitled returns true and your next request for the paid files is authorized without a reload.
Godot signal alternative. If you prefer signals over await, these calls also emit got_is_entitled, got_entitlements, and paywall_resolved when their response arrives.
Fetching the paid files
The paid files ship inside your build, but Wavedash doesn't serve them until the player owns the content — that request is the real lock. Every request for a build file is checked against your offer's glob patterns, and a locked one comes back HTTP 403 with a JSON body listing the contentIdentifiers the player is missing, which is exactly what you need to open the right paywall.
The examples above call a fetch_paid_assets() helper. How you write it depends on the engine: Godot ships the fetch as download_content, Unity normally lets Addressables or an AssetBundle make the request for you, and in JavaScript you fetch the file yourself.
func fetch_paid_assets() -> bool:
var result = await WavedashSDK.download_content("full-version/full.pck")
if result.code == 403 and not result.content_identifiers.is_empty():
# The player doesn't own it yet — offer the purchase, then retry.
var purchase = await WavedashSDK.trigger_paywall(result.content_identifiers[0])
if purchase.success and purchase.data:
result = await WavedashSDK.download_content("full-version/full.pck")
if not result.success:
push_error(result.message)
return false
return ProjectSettings.load_resource_pack(result.data)
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
// Addressables fetches the bundle itself, and Wavedash gates that request like
// any other build file — so settle ownership first, then load.
async void LoadFullVersion()
{
if (!await Wavedash.SDK.IsEntitled("full-version"))
{
bool purchased = await Wavedash.SDK.TriggerPaywall("full-version");
if (!purchased) return;
}
var handle = Addressables.LoadAssetAsync<GameObject>("FullVersionLevel");
var level = await handle.Task;
if (handle.Status != AsyncOperationStatus.Succeeded)
{
Debug.LogError($"Load failed: {handle.OperationException?.Message}");
return;
}
Instantiate(level);
}
async function fetchPaidAssets(itemPath) {
let res = await fetch(`/${itemPath}`);
if (res.status === 403) {
// Locked — the body lists what the player is missing, so offer that.
const { contentIdentifiers } = await res.json();
const purchase = await Wavedash.triggerPaywall(contentIdentifiers[0]);
if (!purchase.success || !purchase.data) return null;
res = await fetch(`/${itemPath}`); // retry now that they own it
}
if (!res.ok) throw new Error(`${itemPath}: HTTP ${res.status}`);
return new Uint8Array(await res.arrayBuffer());
}
Four things hold whichever engine you're in:
- Ask for the path your globs match. That's the file's path from your build root —
full-version/full.pck— not an engine resource path likeres://…orAssets/…. - Don't attach credentials. The player's session rides along as an
httpOnlycookie, so a plain request from your own build is already authenticated. There's no token to add, and nothing for your game code to read. - Retry, don't reload. Ownership refreshes as soon as the paywall resolves, and the
403is sentno-cache, so re-requesting the same URL succeeds in the same session. - Loads your engine starts on its own count too. An Addressables bundle, a streamed audio clip, a texture a scene references — each one is a build-file request, so each is gated the same way. That's what lets Unity's normal loading path work untouched, and it's also why anything your free portion needs must sit outside the locked patterns.
Godot specifics. download_content saves to user:// + item_path; pass a second argument to save it somewhere else:
await WavedashSDK.download_content("full-version/full.pck", "user://dlc/full.pck")
Alongside the usual success and message, the response carries:
| Field | Description |
|---|---|
data | Where the file was saved. Feed it to ProjectSettings.load_resource_pack() for a .pck, or to FileAccess / Image.load_from_file() for loose assets. |
code | The HTTP status, or 0 if the request never left the client. |
content_identifiers | On 403, the identifiers the player must own. Pass one to trigger_paywall. |
Nothing about this is paywall-specific: it also works for content you simply kept out of the initial download, like extra levels or high-resolution texture packs.
download_content runs in Web builds only — in the editor it resolves with success: false and a message saying so.
Signal alternative. The call also emits content_downloaded with the same response, if you'd rather not await it:
func _ready():
WavedashSDK.content_downloaded.connect(func(r): print("Saved to: ", r.data))
Unity specifics. Addressables surfaces a locked bundle as a failed operation, and the 403 response body never reaches your code — there's nothing to read contentIdentifiers out of. That's why the example settles ownership with IsEntitled and TriggerPaywall before loading instead of reacting to the failure. The server-side check still stands behind it; the entitlement call is just what lets you show a paywall instead of a load error.
Lock the content by folder rather than by bundle filename. Addressables can append a content hash to bundle names, so a pattern written against an exact filename stops matching after a rebuild — give the paid group its own build path and glob **/full-version/**.
For a loose file that isn't packed into a bundle — a video, a level blob, a texture pack — request it yourself, and you do get the identifiers back on a 403:
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
[Serializable]
class LockedContent { public string[] contentIdentifiers; }
// Build files are served from the origin the game runs on.
static string ContentUrl(string itemPath) =>
new Uri(new Uri(Application.absoluteURL), "/" + itemPath.TrimStart('/')).ToString();
IEnumerator FetchPaidAssets(string itemPath, Action<byte[]> onReady)
{
for (var attempt = 0; attempt < 2; attempt++)
{
using (var request = UnityWebRequest.Get(ContentUrl(itemPath)))
{
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
onReady(request.downloadHandler.data); // e.g. AssetBundle.LoadFromMemory(bytes)
yield break;
}
if (request.responseCode != 403 || attempt == 1)
{
Debug.LogError($"{itemPath}: {request.error}");
yield break;
}
// Locked — the body lists what the player is missing, so offer that.
var locked = JsonUtility.FromJson<LockedContent>(request.downloadHandler.text);
var purchase = Wavedash.SDK.TriggerPaywall(locked.contentIdentifiers[0]);
yield return new WaitUntil(() => purchase.IsCompleted);
if (!purchase.Result) yield break;
}
}
}
Example: gating the full version
Check ownership on load, then open the paywall when the player taps the locked content; in both paths, fetch the paid files before updating the UI.
func _ready():
var result = await WavedashSDK.is_entitled("full-version")
if result.success and result.data:
await fetch_paid_assets()
set_full_version_unlocked(true)
else:
set_full_version_unlocked(false)
func on_locked_track_pressed():
var result = await WavedashSDK.trigger_paywall("full-version")
if result.success and result.data:
await fetch_paid_assets()
set_full_version_unlocked(true)
async void Start()
{
bool owned = await Wavedash.SDK.IsEntitled("full-version");
if (owned)
await FetchPaidAssets();
SetFullVersionUnlocked(owned);
}
public async void OnLockedTrackPressed()
{
bool purchased = await Wavedash.SDK.TriggerPaywall("full-version");
if (purchased)
{
await FetchPaidAssets();
SetFullVersionUnlocked(true);
}
}
const owned = await Wavedash.isEntitled("full-version");
if (owned.success && owned.data) await fetchPaidAssets();
setFullVersionUnlocked(owned.success && owned.data);
async function onLockedTrackPressed() {
const result = await Wavedash.triggerPaywall("full-version");
if (result.success && result.data) {
await fetchPaidAssets();
setFullVersionUnlocked(true);
}
}