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


