Photo by Jehyun Sung on Unsplash
I recently helped with integrating Spotify APIs into a Wordpress website for a collective of musicians. The ask was for a public user to be able to follow a group of artists on Spotify through a single button click. It seemed to be simple enough, but as all engineers know, reality has a surprising amount of detail. I wasn’t very familiar with PHP and relied heavily on Google. I did this on a Mac running Monterey, but most of the steps (except for website setup) are universally applicable.
To begin with, we need a free or premium Spotify user account. Then, we need to login to the Spotify Developer website, and create an app. Give it a name and a description.

Then, hit Edit Settings on the app:

Enter a callback URL (enter http://localhost/follow/callback.php for now - this is the URL for my local test website) and hit Add, followed by Save at the bottom (this is the link to our website that will be redirected to after Spotify authorization call):

I needed a way for a user to login to Spotify, and authorize my app to follow artists on their behalf. I found some sample code from this tutorial that did just that.
Based on the Authorization Guide, the Authorization Code Flow seemed like what I needed.
“In scenarios where storing the client secret is not safe (e.g. desktop, mobile apps or JavaScript web apps running in the browser), you can use the authorization code with PKCE, as it provides protection against attacks where the authorization code may be intercepted.”
Next, I needed a bulk API to follow artists. I found this one, to which I needed to pass the access token from the authorization call.
curl PUT https://api.spotify.com/v1/me/following?ids=artist1,artist2,...,artistn
-H "Authorization: Bearer BQD.....jwq6c1PBd"
Since it was a Wordpress site, the backend had to be PHP. I created three PHP files - login.php for the authorization, callback.php to handle the callback from Spotify’s authorize endpoint, and follow.php for the artist follow call.
Next, I setup a local PHP web server. PHP comes bundled with Mac OS pre-Monterey. Post-Monterey, it has to be installed using Homebrew. From a terminal window, type (this takes a while to finish):
brew install PHP
There was a lengthy detour to self sign PHP, since Monterey doesn’t accept unsigned packages. This involved setting up a local certificate authority and a code signing certificate.
Then, in the apache root folder (usually /Library/WebServer/Documents), I created a follow folder and dropped the index.html, login.php and follow.php files into it.
index.html is my one page test website. It uses handlebars to populate markup with results from the API calls, and Jquery to call the Spotify APIs.
The Login with Spotify button redirects to login.php:
document.getElementById('login-button').addEventListener('click', function() {
window.location = 'http://localhost/follow/login.php';
}, false);
login.php redirects to Spotify’s authorize endpoint (the response_type must be set to code for the authorization code, and the redirect URI passed has to match the redirect URI in the Spotify app settings) :
$client_id = "<client id from the Spotify app in developer dashboard>";
$state = generateRandomString(16);
$scope = "user-follow-modify";
$url = "https://accounts.spotify.com/authorize";
$url .= "?response_type=code";
$url .= "&client_id=" . urlencode($client_id);
$url .= "&scope=" . urlencode($scope);
$url .= "&redirect_uri=" . urlencode($redirect_uri);
$url .= "&state=" . urlencode($state);
header("Location: " . $url);
Spotify authorize endpoint redirects back to http://localhost/follow/callback.php passing it the authorization code. callback.php exchanges this code for an access token by POSTing to the https://accounts.spotify.com/api/token endpoint. It then calls the /v1/me endpoint with this token to get the user’s display name and user id. It cookies this profile info, encrypts and cookies the access token and redirects back to the index.html page:
// get token & state from url
$error = isset($_GET["error"]) ? $_GET["error"] : NULL;
$code = $_GET["code"];
$state = $_GET["state"];
$url = "/follow";
$client_id = config()["client_id"];
$client_secret = config()["client_secret"];
$redirect_uri = config()["redirect_url"];
$url = "https://accounts.spotify.com/api/token";
$fields = [
"code" => urlencode($code),
"redirect_uri" => $redirect_uri,
"grant_type" => "authorization_code",
"state" => $state
];
//url-ify the data for the POST
$fields_string = http_build_query($fields);
$result = http_request("POST", $url,
array(
'authorization: Basic ' . base64_encode($client_id . ":" . $client_secret)
),
$fields_string);
if ($result !== FALSE && ($result === "" || !array_key_exists("error", json_decode($result,true)))) {
$result_arr = json_decode($result, true);
$access_token = $result_arr["access_token"];
$enc_access_token = encrypt($access_token);
setcookie("t", $enc_access_token, time()+1200);
setcookie("state", $state, time()+1200);
header("Location: /follow");
// get the client-id & name
$url = 'https://api.spotify.com/v1/me';
$result = http_request("GET", $url,
array(
'authorization: Bearer ' . $access_token
));
if ($result !== FALSE && ($result === "" || !array_key_exists("error", json_decode($result,true)))) {
$result_arr = json_decode($result, true);
$display_name = $result_arr["display_name"];
$user_id = $result_arr["id"];
setcookie("display_name", $display_name);
setcookie("user_id", $user_id);
header("Location: /follow");
On the index.html page, we hide the login button, and display the logout and Follow All button.
The Follow All button redirects to follow.php passing in encrypted access_token and user_id:
document.getElementById('follow-button').addEventListener('click', function() {
var url = `/follow/follow.php?access_token=${access_token}&id=${user_id}`;
$.ajax({type: 'GET', url: url})
.done(function(response) {
artistsFollowedPlaceholder.innerHTML = artistsFollowedTemplate(response);
})
.fail(function(msg) {
$('#errorMsg').text(msg.responseJSON.msg);
});
}, false);
follow.php calls the Spotify /follow API. The /follow API is limited to 50 artists per call. I got around that by calling /follow in batches of 50 at a time. Then, I call /artists?ids= to get the artist names and links, which are returned as the response to this call. An admin email is sent as well to track users who followed:
$access_token = decrypt(str_replace(" ", "+", $_GET["access_token"]));
$spotify_id = $_GET["id"];
$artist_ids = config()["artist_ids"];
$api_root_url = "https://api.spotify.com/v1";
$startIdx = 0;
$more = True;
$artist_ids_arr = str_getcsv($artist_ids);
$batch_size = 50;
$result_json = [];
$followed = FALSE;
while ($more) {
$artist_ids_batch = join(",", array_slice($artist_ids_arr, $startIdx, $batch_size));
$url = $api_root_url . "/me/following?type=artist&ids=" . $artist_ids_batch;
$result = http_request("PUT", $url, array('authorization: Bearer ' . $access_token));
if ($result !== FALSE && ($result === "" || !array_key_exists("error", json_decode($result,true)))) {
$url = $api_root_url . "/artists?ids=" . $artist_ids_batch;
$result = http_request("GET", $url, array('authorization: Bearer ' . $access_token));
$result_json = array_merge($result_json, json_decode($result,true)["artists"]);
$followed = True;
}
else {
$followed = False;
$result_json = json_decode("{\"msg\": \"" . "Follow artists failed with error: " . json_decode($result,true)["error"]["message"] . "\"}");
break;
}
$startIdx = $startIdx + $batch_size;
if (count($artist_ids_arr)-1 < $startIdx) {
$more = False;
}
}
if ($followed) {
mail(config()["admin_email"], $spotify_id . "followed all artists on Auricle collective", "");
}
else {
mail(config()["admin_email"], $spotify_id . "Failed to follow all artists on Auricle collective", "");
}
header('Content-Type: application/json; charset=utf-8');
http_response_code($followed ? 200 : 500);
echo(json_encode($result_json));
When I submitted a quota extension request for the app, Spotify asked me to provide a way for users to disconnect from Spotify. I added a logout button which clears cookies and redirects to logout.php:
document.getElementById('logout-button').addEventListener('click', function() {
Cookies.remove("t", { path: '' });
Cookies.remove("state", { path: '' });
Cookies.remove("error", { path: '' });
window.location = "/follow/logout.php";
}, false);
logout.php in turn redirects to Spotify’s logout URL:
<?php
header("Location: https://www.spotify.com/logout/");
?>
This seems to work fine on Chrome. On Safari and Firefox, however, hitting the back button shows the user as logged in. It seems to be loading the page from cache. I tried putting in the usual HTTP no-cache headers, but Safari wouldn’t budge. After some thrashing, I found this snippet on Stackoverflow which did the trick. It reloads the page :
window.addEventListener("pageshow", function(evt){
if(evt.persisted){
setTimeout(function(){
window.location.reload();
},10);
}
}, false);
The flow looks like this:

After publishing this code to your server, update the redirect URI in the Developer dashboard and in login.php.
The HTTP calls in PHP should ideally be asynchronous, but I don’t know enough PHP to do that and this app isn’t expected to have very high volume, so synchronous is good enough.
The Spotify app starts out in Development mode. This means that any user other than yourself has to be explicitly granted access to the app through the Developer dashboard in order to use it. Once you have tested it, you can submit a quota extension request to make it publicly accessible for all users. The first time I did this, Spotify rejected it and asked me to provide a logout feature, add their logo, and provide links to artist content on Spotify. I had to resubmit and am still waiting to hear back.