NEWA drop-in replacement for AssetService:SearchAudioAsyncRead the docs →
← API Reference
Getting started

Luau SDK

v0.19.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.


On this pageInstallation

Installation

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

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

Then run:

Shell
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 give it your API key. Use the Roblox Secrets Store in production to keep your key secure.

Luau (Roblox)
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")

AudioScape.setApiKey(apiKey)

Pointing at another host

Only needed to develop against a locally-running or staging API. Omitted fields keep their current value, and it's safe to call before or after setApiKey.

Luau (Roblox)
-- Only needed to develop against a local or staging API
AudioScape.setEndpoints({
    baseUrl = "http://localhost:3000/developer",
    analyticsUrl = "http://localhost:3001/analytics",
})

Roblox Studio can reach http://localhost, but a published Roblox server cannot. This is a development affordance, not a deployment mechanism.

Similar

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

Luau (Roblox)
local result, err = AudioScape: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.

Luau (Roblox)
-- List all genres — each item exposes a URL-safe slug alongside the canonical name
local genres, err = AudioScape:browse({ type = "genre" })

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

-- Get tracks for a specific genre. `name` accepts the slug (e.g. "hip-hop-rap")
-- or the canonical (e.g. "Hip Hop / Rap"). Slugs are stable URL-safe ids; pass
-- back genre.slug from the list response to round-trip cleanly.
--
-- `sort` orders results within the drill-down: "popular" (default — global
-- popularity ranking, omits tracks with no engagement), "alpha" (track name
-- A→Z), "recent" (newest first). Pick alpha or recent to see fresh uploads.
local tracks, err = AudioScape:browse({
    type = "genre",
    name = "electronic",  -- or genres.items[i].slug
    limit = 20,
    sort = "popular",
})

Browse types: artist, album, genre, mood

Lookup

Resolve asset IDs you already know, up to 100 per request. Use this rather than a search when you have the IDs — it bypasses filters and preserves input order.

Luau (Roblox)
-- Resolve up to 100 known asset IDs in one request
local result, err = AudioScape:lookup({
    asset_ids = { "1843209165", "9120386436", "1234567890" },
})
-- Tracks come back in input order; filters are bypassed.
-- IDs that didn't resolve are listed separately:
print(result.meta.missing_ids)  -- { "1234567890" }

Sound Effects

The SFX catalog has its own methods, matched on how a sound actually sounds rather than on its filename. Categories follow the UCS taxonomy.

Luau (Roblox)
-- Free-text search, or browse a UCS category
local result, err = AudioScape:sfxSearch({
    query = "metal sword impact short",
    limit = 20,
    filters = {
        categories = { "WEAPON" },
        subcategories = { "SWORD" },
        duration = { min = 0, max = 1 },
    },
})

-- Acoustically similar sound effects
local similar = AudioScape:sfxSimilar({ asset_id = "9120386436", limit = 10 })

-- Popularity-ranked, refreshed daily
local trending = AudioScape:sfxBrowse({ type = "trending", limit = 50 })

-- The full broader_category -> category -> subcategory tree, for picker UIs
local taxonomy = AudioScape:getSfxTaxonomy()

Playlists

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

Luau (Roblox)
-- List all configured playlists
local list, err = AudioScape:listPlaylists()

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

-- Fetch a specific playlist
local result, err = AudioScape: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.

Sound Banks

A pool of interchangeable sounds behind one name, so a repeated effect stops sounding repeated. Build it from an asset you already have, or from a bank curated in the console.

Luau (Roblox)
-- From an asset you already have
local bank = AudioScape:createSoundBank({
    seeds = { footstep = "rbxassetid://1837879082" },
    kind = "sfx",
})

-- ...or from a bank curated in the portal
local bank = AudioScape:createSoundBank({ playlist_id = "sfx-1785256484251" })

bank:resolveAsync()
sound.SoundId = "rbxassetid://" .. bank:pick("footstep")

pick never returns the same asset twice in a row, and picks are local table lookups — safe to call on every footstep. See Sound Banks for pools, healing, and trim points.

AssetService Drop-in

If you already call AssetService:SearchAudioAsync, you can point it at AudioScape by changing one line. Same params, same pages, same loop.

Luau (Roblox)
-- Drop-in for AssetService:SearchAudioAsync — one line changes
local AssetService = AudioScape:getAssetService()

local params = Instance.new("AudioSearchParams")
params.SearchKeyword = "rainy night jazz"

for _, audio in AssetService:SearchAudioAsync(params):GetCurrentPage() do
    print(audio.Title, audio.Artist)
end

Every other AssetService member forwards straight through, and when we can't answer, Roblox's own search does. See the drop-in docs.

Asset Health

Audio gets moderated. These tell you which assets your game references are still playable — before a player finds out for you.

Luau (Roblox)
-- Are these assets still playable?
local health = AudioScape:checkAssetHealth({ "1837879082", "9046863579" })
for _, asset in health.assets do
    print(asset.asset_id, asset.status, asset.name)
end

-- Or scan the whole place for Sound and AudioPlayer instances
local audit = AudioScape:auditAudio()
print(audit.meta.distinct_assets .. " distinct audio assets")
print(audit.meta.unavailable .. " unavailable")
StatusMeaning
okServable, and we can offer similarity and variation for it
moderatedTaken down by Roblox moderation
deletedDeleted
delistedRemoved from public listing
privateA real asset your experience can play, but private to you — so we hold no copy
unknownNeither our catalog nor Roblox returns anything for it

auditAudio walks the DataModel, so treat it as a startup or development diagnostic rather than something to poll.

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.

Luau (Roblox)
-- One fetch — helpers reuse the cached structure
local structure, err = AudioScape: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 = AudioScape:sectionAtTime("1843209165", currentTime)
if section and section.label == "Drop" then
    camera:Shake(section.energy)
end

Helpers: AudioScape:beatAtTime(asset_id, t) returns the closest beat { index, time, beat_num, is_downbeat }; AudioScape: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.

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

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

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

Configuration

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

Luau (Roblox)
AudioScape: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.

Luau (Roblox)
local result, err = AudioScape: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")

AudioScape.setApiKey(apiKey)

-- List all genres
local genres, err = AudioScape: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. picked.slug (URL-safe) and picked.name
-- (canonical) both work; slug is preferred for stable URLs.
local picked = genres.items[math.random(#genres.items)]
local tracks, trackErr = AudioScape:browse({
    type = "genre",
    name = picked.slug,
    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")

AudioScape.setApiKey(apiKey)

local function playNext(currentAssetId)
    local result, err = AudioScape: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")

AudioScape.setApiKey(apiKey)

-- List all playlists for this API key
local list, listErr = AudioScape: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 = AudioScape: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")

AudioScape.setApiKey(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 = AudioScape: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.