kimi • Jun 05, 2026

Getting Spotify Now Playing

I like having a Now Playing section on my site. Here’s a quick reference on how to set it up so I can always come back to it.

We need an API route that returns the track currently playing on Spotify.

Authorisation

  1. Create an app in the Spotify Developer portal, use http://127.0.0.1:4321 for the callback URL and select Web API. (Note: 4321 is Astro’s default port, change this if your development setup uses a different port like 3000). Copy the client_id.
  2. Make a GET request to the following URL (replace YOUR_CLIENT_ID and adjust the port if necessary):
https://accounts.spotify.com/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=http://127.0.0.1:4321&scope=user-read-currently-playing
  1. Authorise the application.
  2. It will redirect you to your local app with a long URL. Copy the code parameter from the URL.
  3. Create a simple script to get the refresh token. For example, encode.py:
import base64
import json
import urllib.request
import urllib.parse

AUTH_CODE = "YOUR_AUTHORISATION_CODE_FROM_STEP_4"
REDIRECT_URI = "http://127.0.0.1:4321"

client_id = 'YOUR_CLIENT_ID'
client_secret = 'YOUR_CLIENT_SECRET'

base64_credentials = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()

def get_spotify_token():
    params = {
        "grant_type": "authorization_code",
        "code": AUTH_CODE,
        "redirect_uri": REDIRECT_URI
    }

    headers = {
        "Content-Type": "application/x-www-form-urlencoded",
        "Authorization": f"Basic {base64_credentials}"
    }

    data = urllib.parse.urlencode(params).encode()
    req = urllib.request.Request(
        "https://accounts.spotify.com/api/token",
        data=data,
        headers=headers,
        method="POST"
    )
    
    with urllib.request.urlopen(req) as response:
        resp_data = response.read()

    print(json.loads(resp_data.decode()))

get_spotify_token()

The response will print out your tokens, including the refresh_token. Save this!

Serverless API Route

Add your credentials to your .env file:

CLIENT_ID=YOUR_CLIENT_ID
CLIENT_SECRET=YOUR_CLIENT_SECRET
REFRESH_TOKEN=YOUR_REFRESH_TOKEN

Since I didn’t want to turn my static site into a server-side rendered (SSR) application, I opted to use a Vercel serverless function to handle the token exchange securely. (If this were a Next.js project, I’d just drop this into an /api route).

I like to keep my Spotify logic in a helper file, for example src/lib/spotify.ts:

// src/lib/spotify.ts
import querystring from 'querystring';

const clientID = import.meta.env.CLIENT_ID;
const clientSecret = import.meta.env.CLIENT_SECRET;
const refreshToken = import.meta.env.REFRESH_TOKEN;

const TOKEN_URL = `https://accounts.spotify.com/api/token`;
const basicAuth = Buffer.from(`${clientID}:${clientSecret}`).toString('base64');

const getAccessToken = async () => {
	const response = await fetch(TOKEN_URL, {
		method: 'POST',
		headers: {
			Authorization: `Basic ${basicAuth}`,
			'Content-Type': 'application/x-www-form-urlencoded'
		},
		body: querystring.stringify({
			grant_type: 'refresh_token',
			refresh_token: refreshToken
		})
	});

	return response.json();
};

const NOW_PLAYING_URL = `https://api.spotify.com/v1/me/player/currently-playing`;

export const getNowPlaying = async () => {
	const { access_token } = await getAccessToken();

	return fetch(NOW_PLAYING_URL, {
		headers: {
			Authorization: `Bearer ${access_token}`
		}
	});
};

And expose it via the serverless function endpoint api/spotify.ts:

// api/spotify.ts
import type { VercelRequest, VercelResponse } from '@vercel/node';
import path from 'path';

// Vercel's serverless functions can't import files above the /api directory directly with relative paths like '../src/lib/spotify'.
// Instead, use an absolute path using the process.cwd(), e.g.,
// https://vercel.com/kb/guide/how-can-i-use-files-in-serverless-functions

// Dynamically import the file using an absolute path (ESM compatible)
const spotifyLibPath = path.join(process.cwd(), 'src', 'lib', 'spotify.js');

export interface SpotifyPlayerResponse {
  is_playing: boolean;
  item: {
    name: string;
    artists: { name: string }[];
    album: {
      name: string;
      images: { url: string }[];
    };
    external_urls: {
      spotify: string;
    };
    duration_ms: number;
  };
  progress_ms: number;
}

export default async function handler(req: VercelRequest, res: VercelResponse) {
  // Use dynamic import to load the module in ESM
  const { getNowPlaying } = await import(spotifyLibPath);

  const response = await getNowPlaying();

  if (response.status === 204 || response.status > 400) {
    res.setHeader('Content-Type', 'application/json');
    res.status(200).send(JSON.stringify({ isPlaying: false }));
    return;
  }

  const player = (await response.json()) as SpotifyPlayerResponse;
  const isPlaying = player.is_playing;
  const title = player.item.name;
  const artist = player.item.artists.map((_artist) => _artist.name).join(', ');
  const album = player.item.album.name;
  const albumImageUrl = player.item.album.images[0].url;
  const songUrl = player.item.external_urls.spotify;
  const duration = player.item.duration_ms;
  const progress = player.progress_ms;

  res.setHeader('Content-Type', 'application/json');
  res.status(200).send(
    JSON.stringify({
      isPlaying,
      title,
      artist,
      album,
      albumImageUrl,
      songUrl,
      duration,
      progress,
    })
  );
}

Wrapping Up

With this API route ready, we can fetch the isPlaying status and track info from any frontend component. Keeping it serverless means my site stays fast and static, but I still get live data.

(Reference: Tommy Palmer’s original guide)