← API Reference

Luau SDK

v0.7.0

Drop-in Wally package for Roblox — search, browse, discover music, and track player analytics in a few lines of Luau. Runs server-side with automatic batching and game telemetry.


Installation

Install via Wally, the Roblox package manager. Add the dependency to your wally.toml:

wally.toml
[dependencies]
AudioScape = "this-fifo/audioscape-sdk@0.7.0"

Then run:

wally install

Alternatively, download AudioScape.rbxm from the latest release and drop it into ServerStorage in Roblox Studio.

Source code and examples available on GitHub.

Prerequisites: Enable Allow HTTP Requests in Game Settings → Security. The SDK uses HttpService:RequestAsync() and must run in a server Script (not a LocalScript). Roblox enforces a limit of 500 HTTP requests per minute per game server.

Setup

Require the module and create a client with your API key. Use the Roblox Secrets Store in production to keep your key secure.

Luau
local ServerScriptService = game:GetService("ServerScriptService")
local HttpService = game:GetService("HttpService")
local RunService = game:GetService("RunService")

local AudioScape = require(ServerScriptService.Packages.AudioScape)

-- Use a test key in Studio, Secrets Store in production
local apiKey = if RunService:IsStudio()
    then "your-test-key"
    else HttpService:GetSecret("AudioScapeKey")

local client = AudioScape.new(apiKey)

Similar

Find tracks that sound like a given track. Pass an asset ID and get acoustically similar results.

local result, err = client:similar({
    asset_id = "123456789",
    limit = 5,
    playerId = player.UserId,
})

if result then
    print("Found", result.meta.total, "similar tracks")
end

Browse

Browse by artist, album, genre, or mood. Omit name to list available entities, or provide it to get tracks.

-- List all genres
local genres, err = client:browse({ type = "genre" })

for _, genre in genres.items do
    print(genre.display_name, "—", genre.track_count, "tracks")
end

-- Get tracks for a specific genre
local tracks, err = client:browse({
    type = "genre",
    name = "electronic",
    limit = 20,
})

Browse types: artist, album, genre, mood

Playlists

Fetch playlists configured in the Configure tab. List all playlists for your API key, or fetch a specific playlist with its tracks.

-- List all configured playlists
local list, err = client:listPlaylists()

for _, p in list.playlists do
    print(p.name, "—", p.track_count, "tracks")
end

-- Fetch a specific playlist
local result, err = client:getPlaylist({
    playlist_id = list.playlists[1].id,
})

-- result.playlist = { id, name, genre, playback_mode, track_count }
-- result.tracks = { { asset_id, name, artist, position, ... } }

Playlist IDs are found in the Configure tab or via listPlaylists(). See the Playlist endpoint docs for the full API reference.

Track Structure

Fetch the beat grid and section labels for a track to sync animations, lighting, or VFX to the music. Returns BPM, every beat in seconds, downbeats, and labelled sections (Intro/Verse/Drop/Climax/...) with energy 1-4 and bar ranges. See the Track Structure endpoint docs for the full response shape.

-- One fetch — helpers reuse the cached structure
local structure, err = client:getStructure({ asset_id = "1843209165" })

-- Burst particles on every downbeat
for _, t in ipairs(structure.beat_grid.downbeats) do
    task.delay(t, function() emitter:Emit(20) end)
end

-- Trigger different VFX on the Drop
local section = client:sectionAtTime("1843209165", currentTime)
if section and section.label == "Drop" then
    camera:Shake(section.energy)
end

Helpers: client:beatAtTime(asset_id, t) returns the closest beat { index, time, beat_num, is_downbeat }; client:sectionAtTime(asset_id, t, level?) returns the section (or phrase) covering time t. Both share a per-asset client-side cache, so repeated calls are free after the first fetch.

Coverage: ~96% of the catalog has beat grid analysis, ~91% has section labels. Tracks without analysis return null for beat_grid and empty arrays for sections/phrases.

Telemetry

The SDK automatically sends your game's Universe ID and Place ID with every request via headers. These are captured from game.GameId and game.PlaceId at construction time — no setup needed.

You can also pass an optional playerId to any method to tie requests to specific players for per-player analytics. This data appears in your dashboard under Unique Games, Unique Players, and the Top Games table.

HeaderSourceSent
X-Universe-Idgame.GameIdEvery request (auto)
X-Place-Idgame.PlaceIdEvery request (auto)
X-Player-Idplayer.UserIdWhen playerId is provided

Analytics

Track player behavior to power music intelligence — trending charts, personalized recommendations, and search relevance. Events are automatically batched and sent in the background with no impact on game performance.

-- Track when a player listens to a song
client:trackPlay(track.asset_id, player.UserId, songDuration)

-- Track votes, favorites, skips
client:trackVote(assetId, "up", player.UserId)
client:trackFavorite(assetId, player.UserId)
client:trackSkip(assetId, player.UserId, 12.5)

-- Track custom events
client:trackCustom("station_tuned", assetId, player.UserId, {
    station = "main_stage",
})

Configuration

Analytics is enabled by default with sensible defaults. Override settings if needed:

client:configureAnalytics({
    enabled = true,           -- default true
    batchInterval = 30,           -- seconds between flushes
    maxBatchSize = 50,           -- events per batch
    maxQueueSize = 500,          -- max buffered events
})
MethodSignalDescription
trackPlayMediumSong started playing
trackStopInfoSong stopped (natural end or user action)
trackSkipNegativeSong skipped early (pass duration for signal strength)
trackVoteHighUpvote or downvote ("up" / "down")
trackFavoriteVery HighAdded to favorites
trackUnfavoriteInfoRemoved from favorites
trackAddToQueueHighAdded to setlist or queue
trackSearchClickMediumClicked a search result
trackCustomCustomAny custom event with optional metadata

Privacy: The playerId parameter is optional on all tracking methods. Omit it for fully anonymous analytics, or pass a hashed value if you need per-player insights without exposing real player IDs. Aggregate metrics (top tracks, trending, vote ratios) work without any player identification.


Error Handling

All methods return result, err. On failure, result is nil and err is a descriptive string.

local result, err = client:search({ query = "test" })

if not result then
    warn("AudioScape error:", err)
    -- err = "Rate limited — too many requests (429)"
    return
end

Examples

Complete scripts you can drop into ServerScriptService. Each one is self-contained and ready to run.

Browse genres and play a track

Lists all available genres, picks one at random, fetches its tracks, and plays a random track via SoundService.

BrowseGenres.luau
local ServerStorage = game:GetService("ServerStorage")
local SoundService = game:GetService("SoundService")
local HttpService = game:GetService("HttpService")
local RunService = game:GetService("RunService")

local AudioScape = require(ServerStorage.AudioScape)

local apiKey = if RunService:IsStudio()
    then "your-test-key"
    else HttpService:GetSecret("AudioScapeKey")

local client = AudioScape.new(apiKey)

-- List all genres
local genres, err = client:browse({ type = "genre" })
if not genres then warn("Failed:", err) return end

for _, genre in genres.items do
    print(genre.display_name, "—", genre.track_count, "tracks")
end

-- Pick a genre and get its tracks (genre names are case-sensitive — use what listGenres returns)
local picked = genres.items[math.random(#genres.items)]
local tracks, trackErr = client:browse({
    type = "genre",
    name = picked.name,
    limit = 10,
})
if not tracks then warn("Failed:", trackErr) return end

-- Play a random track
local track = tracks.tracks[math.random(#tracks.tracks)]
print("Playing:", track.artist, "—", track.name)

local sound = Instance.new("Sound")
sound.SoundId = "rbxassetid://" .. track.asset_id
sound.Parent = SoundService
sound:Play()

Auto-playlist with similar tracks

When the current track ends, finds acoustically similar tracks and plays one. Add any Sound to SoundService to seed the chain.

SimilarTrack.luau
local ServerStorage = game:GetService("ServerStorage")
local SoundService = game:GetService("SoundService")
local HttpService = game:GetService("HttpService")
local RunService = game:GetService("RunService")

local AudioScape = require(ServerStorage.AudioScape)

local apiKey = if RunService:IsStudio()
    then "your-test-key"
    else HttpService:GetSecret("AudioScapeKey")

local client = AudioScape.new(apiKey)

local function playNext(currentAssetId)
    local result, err = client:similar({
        asset_id = currentAssetId,
        limit = 5,
    })
    if not result or #result.tracks == 0 then
        warn("No similar tracks:", err or "empty")
        return
    end

    local next = result.tracks[math.random(#result.tracks)]
    print("Up next:", next.artist, "—", next.name)

    local sound = Instance.new("Sound")
    sound.SoundId = "rbxassetid://" .. next.asset_id
    sound.Parent = SoundService

    sound.Ended:Once(function()
        sound:Destroy()
        playNext(next.asset_id)
    end)

    sound:Play()
end

-- Start from any Sound already in SoundService
local seed = SoundService:FindFirstChildWhichIsA("Sound")
if seed then
    local id = string.match(seed.SoundId, "%d+")
    if id then
        seed.Ended:Once(function() playNext(id) end)
    end
end

Play a configured playlist

Fetches a playlist created in the Configure tab and plays its tracks sequentially. Respects shuffle mode.

PlaylistStation.luau
local ServerStorage = game:GetService("ServerStorage")
local SoundService = game:GetService("SoundService")
local HttpService = game:GetService("HttpService")
local RunService = game:GetService("RunService")

local AudioScape = require(ServerStorage.AudioScape)

local apiKey = if RunService:IsStudio()
    then "your-test-key"
    else HttpService:GetSecret("AudioScapeKey")

local client = AudioScape.new(apiKey)

-- List all playlists for this API key
local list, listErr = client:listPlaylists()
if not list or #list.playlists == 0 then
    warn("No playlists found:", listErr or "none configured")
    return
end

-- Fetch the first playlist's tracks
local result, err = client:getPlaylist({ playlist_id = list.playlists[1].id })
if not result then warn("Failed:", err) return end

-- Shuffle if configured
local tracks = result.tracks
if result.playlist.playback_mode == "shuffle" then
    for i = #tracks, 2, -1 do
        local j = math.random(i)
        tracks[i], tracks[j] = tracks[j], tracks[i]
    end
end

-- Play tracks sequentially
for _, track in tracks do
    print("Now playing:", track.artist, "—", track.name)

    local sound = Instance.new("Sound")
    sound.SoundId = "rbxassetid://" .. track.asset_id
    sound.Parent = SoundService
    sound:Play()
    sound.Ended:Wait()
    sound:Destroy()
end

print("Playlist finished!")

Search box with RemoteEvent

Listens for search requests from clients via a RemoteEvent. Create a RemoteEvent named SearchRequest in ReplicatedStorage, then fire it from a LocalScript with the query string.

SearchBox.luau — Server Script
local ServerStorage = game:GetService("ServerStorage")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local HttpService = game:GetService("HttpService")
local RunService = game:GetService("RunService")

local AudioScape = require(ServerStorage.AudioScape)

local apiKey = if RunService:IsStudio()
    then "your-test-key"
    else HttpService:GetSecret("AudioScapeKey")

local client = AudioScape.new(apiKey)

local searchEvent = ReplicatedStorage:WaitForChild("SearchRequest")

searchEvent.OnServerEvent:Connect(function(player, query)
    if type(query) ~= "string" or #query == 0 then return end

    local result, err = client:search({
        query = query,
        limit = 10,
        playerId = player.UserId,
    })

    if not result then
        warn("Search failed for", player.Name, ":", err)
        return
    end

    print(player.Name, "searched", query, "—", result.meta.total, "results")

    -- Send results back to client for display
    searchEvent:FireClient(player, result)
end)

Source code for all examples available on GitHub.