Not sure what I should’ve listed this post as but I’m pretty sure this works?
SHOWCASE VIDEO AT THE BOTTOM … or here
.
.
I had recently started to get into API’s in Roblox which had sparked a great idea for a game, a social hangout where people can see what others are listening to on Spotify, this would have been very interactive and cool to see on Roblox.
API Limitations & Why This Isn’t Fully Public
Having seen the Spotify API, I knew my game could be done.
And I had done it, within one day, I had made the game. But with a major flaw, reading the new Spotify API TOS if I wanted to go mainstream I would need a certain upgrade to my API, only issue. I need to fill a certain criteria.
Easy to see, as a single Roblox developer I have no chance to be able to use the full API, which had only recently been updated to have criteria needs.
But alas with my developer API I was able to host 25 players if I had their Email within the API database, so for fun I was able to play around with it with some of my friends.
Backend: Bridging Roblox & Spotify | How It Works ( Off Platform )
Since Roblox doesn’t support direct Spotify integration, I built an Express.js (Node.js) server to act as a middleman between Spotify’s Web API and Roblox’s HttpService.
The backend handler works with Node.js
It handles:
- Handle OAuth login/token storage
- Query Spotify’s
/me/playerendpoint - Return playback data (track, artist, progress) in JSON
- Hosting the endpoint via Render.com ( Seemed to have free hosting for small projects )
server.js (Express server):
// Example endpoint
app.get("/spotify/status/:userid", async (req, res) => {
const userId = req.params.userid;
const token = getTokenForUser(userId); // Stored access token
const response = await axios.get("https://api.spotify.com/v1/me/player", {
headers: { Authorization: `Bearer ${token}` }
});
const body = response.data;
res.json({
track: body.item?.name || null,
artist: (body.item?.artists || []).map(a => a.name).join(', ') || null,
is_playing: body.is_playing || false,
album: body?.item?.album?.name,
duration_ms: body?.item?.duration_ms,
progress_ms: body?.progress_ms
});
});
How It Works:
- Roblox calls
https://yourserver.com/spotify/status/"USERID" - Server looks up
USERID’s access token - Makes request to Spotify’s
/me/playerendpoint - Sends JSON with track, artist, progress, etc.
Hosting:
- Code pushed to GitHub
- Deployed on Render.com for live hosting ( For a Temporary Time )
- If the project ever scaled, I planned to host it on my own or a friend’s domain
Roblox Integration
Responsibilities:
- Listen for RemoteEvents
- Request data from the backend
- Attach a floating display UI to the player’s
Head - Continuously update the UI as songs progress
ServerScript part
players.PlayerAdded:Connect(function(p)
p.CharacterAdded:Wait()
module.start(p, 'cre') -- Create on spawn
end)
sendEvent.OnServerEvent:Connect(function(p)
module.start(p, 'cre') -- Manual create
end)
updateEvent.OnServerEvent:Connect(function(p)
p.Character.Head:WaitForChild("SpotifyDisplay"):Destroy()
module.start(p, 'cre') -- Refresh
end)
3 ways to get the display
- Joining ( Account Linked Prior )
startbutton being pressed after linkingupdatebutton being pressed, given to players after confirmed linkage (Just a refresh button)
Automating It
To avoid flooding the backend with requests, I implemented a timer-based updater in a ModuleScript. It animates the song progress locally, then fetches new data when the song ends.
Example:
local function createTimer(player, label: TextLabel, currentMS, durMS)
task.spawn(function()
local cur = math.floor(currentMS / 1000)
local dur = math.floor(durMS / 1000)
for i = cur, dur - 1 do
if label == nil or not label:IsDescendantOf(game) then break end
label.Text = "Progress: " .. formatTime(i * 1000) .. " / " .. formatTime(dur * 1000)
wait(1)
end
print("END — scheduling next update")
getSpotifyStatus(player, "upd")
end)
end
Once the timer had ended It would call the main function
function getSpotifyStatus(player: Player, request)
local userId = tostring(player.UserId)
local url = "https://spotify-to-roblox.onrender.com/spotify/status/" .. userId -- The hosted Roblox to Spotify Linker
local success, result = pcall(function()
return HttpService:GetAsync(url)
end)
if not success then
warn("Failed to fetch Spotify status:", result)
player.PlayerGui.MainGui.linkProfile.Visible = true
return
end
-- print("Raw response:", result)
local data
local decodeSuccess, decodeError = pcall(function()
data = HttpService:JSONDecode(result)
end)
if not decodeSuccess then
warn("JSON decoding failed:", decodeError)
return
end
if data.error then
warn("Spotify API error:", data.error)
elseif typeof(data.track) == "string" and typeof(data.artist) == "string" then
if request == "cre" then
createDisplay(player, data)
elseif request == "upd" then
local head = player.Character and player.Character:FindFirstChild("Head")
if head then
local gui = head:FindFirstChild("SpotifyDisplay")
if gui then
updateDisplay(player, gui:FindFirstChild("Frame"), data)
else
warn("No display to update")
end
end
else
warn(request .. " is not a valid Request")
end
player.PlayerGui.MainGui.update.Visible = true
else
print("Invalid or empty data received. Track:", data.track, "Artist:", data.artist)
end
end
View Full Dissection
Purpose:
This function fetches a player’s currently playing Spotify track using their stored user ID and sends the response to either create or update the UI in Roblox.
Dissected Function:
function getSpotifyStatus(player: Player, request)
local url = "https://spotify-to-roblox.onrender.com/spotify/status/" .. player.UserId
request: A string that decides what to do with the data ("cre"= create UI,"upd"= update UI).url: The custom endpoint you made that returns the player’s Spotify playback data.
local success, result = pcall(function()
return HttpService:GetAsync(url)
end)
pcall: Ensures the request won’t crash the script if it fails. It safely captures errors.HttpService:GetAsync(url): Makes an HTTP GET request to the Express server.success: Boolean result of whether the request worked.result: The raw JSON string from the server or an error message.
if not success then
warn("Fetch failed:", result)
player.PlayerGui.MainGui.linkProfile.Visible = true
return
end
- If the request failed (e.g., server offline, no internet), it gives the player an update button to let them try to update their status again.
local data
local decodeSuccess, decodeError = pcall(function()
data = HttpService:JSONDecode(result)
end)
- This decodes the raw JSON string from the backend into a usable Lua table.
- Again, wrapped in
pcallin case the data is invalid/corrupt.
if not decodeSuccess then
warn("JSON decoding failed:", decodeError)
return
end
- If the JSON couldn’t be parsed, the function stops here.
if data.error then
warn("Spotify API error:", data.error)
- Spotify might return an error like
invalid_tokenorno_active_device. - If so, this logs the issue and stops further UI updates.
elseif typeof(data.track) == "string" and typeof(data.artist) == "string" then
- Validates that the track/artist values are present and properly formatted.
- Ensures you’re not trying to update the UI with bad or missing data.
if request == "cre" then
createDisplay(player, data)
- If the request was
"cre", it callscreateDisplay()to attach a new GUI to the player.
elseif request == "upd" then
local gui = player.Character:FindFirstChild("Head")?.SpotifyDisplay
if gui then
updateDisplay(player, gui:FindFirstChild("Frame"), data)
- If the request is
"upd", it finds the player’s existing UI and refreshes the text based on the new data. - Uses optional chaining (
?.) to safely access the child.
else
warn("No display to update")
end
else
warn("Invalid request type")
end
-
Handles fallback if:
- There’s no UI to update.
- Or the request was neither
"cre"nor"upd".
player.PlayerGui.MainGui.update.Visible = true
else
warn("Invalid or empty data received.")
end
end
- If all went well, it makes the
Updatebutton visible. - If the track or artist data is missing or corrupt, logs a warning.
Summary
This function does four big things:
- Fetch current Spotify data via the external API server.
- Parse the JSON response safely.
- Verify the data is valid and meaningful.
- Display or Update a 3D GUI inside Roblox for that player.
It’s built to fail silently.
Data Flow
[Player joins game]
⬇
[Client fires RemoteEvent]
⬇
[Roblox Server fetches /spotify/status/:id]
⬇
[The Node.js server calls Spotify API]
⬇
[Returns JSON: {track, artist, progress_ms...}]
⬇
[Roblox creates/upates GUI + starts timer loop]
In Action
UI
VIDEO – Music is a little strange… bare with me I love all music🙏
NOTE:
This was all done free, so feel free to try it out yourself if you’d like! though you’ll also be limited by Spotify API unless you’re an actual business owner… anyway. let me know if you try it yourself!!!
…
I never finished the game fully as I lost all motive once I found about the API restrictions. Even in the video, though cut out, it is bugged, all in roblox though, as the code was not fully fool proof. linking accounts then playing a song, then unlinking, then relinking, will cause error once the song ends on the games side with the timer. The timer is to blame as its not well put together. This is not the only issue though, there are plenty more, as of any game made. But if this had actually worked as indented for a large scale game, I would have made everything to perfection. But as of now, I end this project here. I hope you found this as fascinating as I did, I loved every second of putting this all together start to finish, its a shame I end it.
…
If you have any questions feel free to ask! im more than willing to answer!
As of posting:

Everything by: @X70PA


