Showcase: Spotify to Roblox display

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.
image
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/player endpoint
  • 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:

  1. Roblox calls https://yourserver.com/spotify/status/"USERID"
  2. Server looks up USERID’s access token
  3. Makes request to Spotify’s /me/player endpoint
  4. 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 )
  • start button being pressed after linking
  • update button 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 pcall in 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_token or no_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 calls createDisplay() 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 Update button visible.
  • If the track or artist data is missing or corrupt, logs a warning.

Summary

This function does four big things:

  1. Fetch current Spotify data via the external API server.
  2. Parse the JSON response safely.
  3. Verify the data is valid and meaningful.
  4. 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:
image
Everything by: @X70PA

4 Likes