The UGC API covers replays, screenshots, custom levels, mods, and other player-owned files.
UGC types
| Type | Value | Use for |
|---|---|---|
SCREENSHOT | 0 | Still captures |
VIDEO | 1 | Clips or recordings |
COMMUNITY | 2 | Levels, mods, maps |
GAME_MANAGED | 3 | Replays, saves |
OTHER | 4 | Anything else |
GAME_MANAGED is intended for items the game controls (e.g. replays attached to leaderboard entries). They aren't hidden from listUGCItems — filter by ugcType if you don't want them in player-facing lists, and fetch replays directly by the ugcId on the leaderboard entry.
Visibility
| Value | Constant | Who sees it |
|---|---|---|
0 | PUBLIC | Everyone |
2 | PRIVATE | The creator only |
Create and upload
Playtest the example project View example project on GitHubfunc _ready():
WavedashSDK.ugc_item_created.connect(_on_ugc_created)
func create_level_share(abs_path: String) -> void:
WavedashSDK.create_ugc_item(
WavedashSDK.Constants.UGC_TYPE_COMMUNITY,
"Custom arena",
"Uploaded from editor",
WavedashSDK.Constants.UGC_VISIBILITY_PUBLIC,
abs_path
)
func _on_ugc_created(response):
if response.get("success", false):
print("ugc id: ", response["data"])string ugcId = await Wavedash.SDK.CreateUGCItem(
ugcType: WavedashConstants.UGCItemType.COMMUNITY,
title: "Custom arena",
description: "Uploaded from editor",
visibility: WavedashConstants.UGCVisibility.PUBLIC,
filePath: localPath
);
Debug.Log($"ugc id: {ugcId}");local co = coroutine.create(function()
local response = wavedash.create_ugc_item_async(
wavedash.UGC_TYPE_COMMUNITY,
"Custom arena",
"Uploaded from editor",
wavedash.UGC_VISIBILITY_PUBLIC,
local_path -- a path written with wavedash.write_local_file_async
)
if response.success then
print("ugc id:", response.data)
end
end)
assert(coroutine.resume(co))// The path is a key in the SDK's local store — write the bytes there first.
await Wavedash.writeLocalFile("ugc/levels/arena.wdc", levelBytes);
const response = await Wavedash.createUGCItem(
Wavedash.UGCType.COMMUNITY,
"Custom arena",
"Uploaded from editor",
Wavedash.UGCVisibility.PUBLIC,
"ugc/levels/arena.wdc"
);
if (response.success) {
console.log("ugc id:", response.data);
}In Godot, the file path may be either user://... (auto-normalized) or an absolute path under OS.get_user_data_dir(). Paths outside the user data directory are rejected.
The file path argument (filePath in JavaScript and Unity, local_file_path in Godot, file_path in Defold) is optional — you can create a UGC item with metadata only and attach a file later with updateUGCItem. This is useful when the file is still being rendered or generated.
// Create the item first
const response = await Wavedash.createUGCItem(Wavedash.UGCType.COMMUNITY, "Custom arena", "WIP");
// ...game renders the level file...
// Attach the file later
if (response.success) {
const ugcId = response.data;
await Wavedash.updateUGCItem(ugcId, { filePath: "ugc/levels/arena.wdc" });
}Update an item
Pass only the fields you want to change. Any field omitted from the updates object/dict is left unchanged on the server.
WavedashSDK.ugc_item_updated.connect(func(response): print("Updated: ", response.success))
WavedashSDK.update_ugc_item(ugc_id, {
"title": "Renamed level",
"description": "Fixed spawn",
"visibility": WavedashSDK.Constants.UGC_VISIBILITY_PRIVATE
})await Wavedash.SDK.UpdateUGCItem(ugcId, title: "Renamed level", description: "Fixed spawn", visibility: WavedashConstants.UGCVisibility.PRIVATE);local co = coroutine.create(function()
-- The Defold binding forwards its second argument as the JS `updates`
-- object, so pass the fields as a JSON string.
wavedash.update_ugc_item_async(ugc_id, json.encode({
title = "Renamed level",
description = "Fixed spawn",
visibility = wavedash.UGC_VISIBILITY_PRIVATE
}))
end)
assert(coroutine.resume(co))await Wavedash.updateUGCItem(ugcId, {
title: "Renamed level",
description: "Fixed spawn",
visibility: Wavedash.UGCVisibility.PRIVATE,
});List items
Pass filters on the first page; pass continueCursor on subsequent pages.
WavedashSDK.got_ugc_items.connect(func(response):
if response.get("success", false):
for item in response["data"]["page"]:
print(item.get("title", ""))
)
var response = await WavedashSDK.list_ugc_items({
"createdBy": user_id,
"ugcType": WavedashSDK.Constants.UGC_TYPE_COMMUNITY,
"titleSearch": "Arena",
"numItems": 50,
})
if response.get("success", false) and not response["data"]["isDone"]:
WavedashSDK.list_ugc_items({ "continueCursor": response["data"]["continueCursor"] })var response = await Wavedash.SDK.ListUGCItems(
createdBy: userId,
ugcType: WavedashConstants.UGCItemType.COMMUNITY,
titleSearch: "Arena",
numItems: 50
);
if (response != null && response.TryGetValue("isDone", out var isDone) && !(bool)isDone)
{
var next = await Wavedash.SDK.ListUGCItems(
continueCursor: (string)response["continueCursor"]
);
}local co = coroutine.create(function()
local response = wavedash.list_ugc_items_async({
createdBy = user_id,
ugcType = wavedash.UGC_TYPE_COMMUNITY,
titleSearch = "Arena",
numItems = 50
})
if response.success and not response.data.isDone then
local next_page = wavedash.list_ugc_items_async({
continueCursor = response.data.continueCursor
})
end
end)
assert(coroutine.resume(co))const response = await Wavedash.listUGCItems({
createdBy: userId,
ugcType: Wavedash.UGCType.COMMUNITY,
titleSearch: "Arena",
numItems: 50,
});
if (response.success) {
for (const item of response.data.page) console.log(item.title);
if (!response.data.isDone) {
await Wavedash.listUGCItems({ continueCursor: response.data.continueCursor });
}
}Delete an item
Deletes a UGC item and frees the user's storage quota by the size of the deleted upload.
# Await the response directly
var response = await WavedashSDK.delete_ugc_item(ugc_id)
print("Deleted: ", response.success)
# Or listen via signal
WavedashSDK.ugc_item_deleted.connect(func(r): print("Deleted: ", r.success))
WavedashSDK.delete_ugc_item(ugc_id)await Wavedash.SDK.DeleteUGCItem(ugcId);local co = coroutine.create(function()
wavedash.delete_ugc_item_async(ugc_id)
end)
assert(coroutine.resume(co))await Wavedash.deleteUGCItem(ugcId);Download content
func _ready():
WavedashSDK.ugc_item_downloaded.connect(func(response): print("Downloaded: ", response.data))
func download_into_user_data(ugc_id: String, file_name: String) -> void:
var dir = OS.get_user_data_dir() + "/downloads"
DirAccess.make_dir_recursive_absolute(dir)
WavedashSDK.download_ugc_item(ugc_id, dir + "/" + file_name)using System.IO; // Path, Directory
var dir = Path.Combine(Application.persistentDataPath, "downloads");
Directory.CreateDirectory(dir);
await Wavedash.SDK.DownloadUGCItem(ugcId, Path.Combine(dir, fileName));local co = coroutine.create(function()
local path = "downloads/" .. file_name
local response = wavedash.download_ugc_item_async(ugc_id, path)
if response.success then
print("Downloaded:", response.data)
end
end)
assert(coroutine.resume(co))const response = await Wavedash.downloadUGCItem(ugcId, "downloads/pack.bin");
if (response.success) {
const bytes = await Wavedash.readLocalFile("downloads/pack.bin");
}Attach UGC to a leaderboard entry
UGC is for payloads — replays, screenshots, ghost data. For a few small values like the character used or the number of deaths, leaderboard entry metadata stores them on the entry directly, with no file to upload or download. The same score upload takes both.
Pass the ugcId into the score upload call:
local co = coroutine.create(function()
local lb = wavedash.get_or_create_leaderboard_async(
"speedrun-times",
wavedash.LEADERBOARD_SORT_ORDER_ASC,
wavedash.LEADERBOARD_DISPLAY_TYPETIME_MILLISECONDS
)
local ugc = wavedash.create_ugc_item_async(
wavedash.UGC_TYPE_GAME_MANAGED,
"Replay",
"replay of level 1",
wavedash.UGC_VISIBILITY_PUBLIC,
"replays/run.dat"
)
if lb.success and ugc.success then
wavedash.upload_leaderboard_score_async(lb.data.id, 12345, true, ugc.data)
end
end)
assert(coroutine.resume(co))const lb = await Wavedash.getOrCreateLeaderboard(
"speedrun-times",
Wavedash.LeaderboardSortOrder.ASC,
Wavedash.LeaderboardDisplayType.TIME_MILLISECONDS
);
await Wavedash.writeLocalFile("replays/run.dat", replayBytes);
const ugc = await Wavedash.createUGCItem(
Wavedash.UGCType.GAME_MANAGED,
"Replay",
"replay of level 1",
Wavedash.UGCVisibility.PUBLIC,
"replays/run.dat"
);
if (lb.success && ugc.success) {
const ugcId = ugc.data;
await Wavedash.uploadLeaderboardScore(lb.data.id, 12345, true, ugcId);
}