librespot
librespot copied to clipboard
[Notice] open.spotify.com/get_access_token No longer works
Look for similar bugs
Please check if there's already an issue for your problem. If you've only a "me too" comment to make, consider if a :+1: reaction will suffice.
Description
A clear and concise description of what the problem is.
I don't know what Spotify changed but open.spotify.com/get_access_token no longer gives an accessToken, giving the error
{
"error": {
"code": 400,
"message": "Invalid TOTP"
}
}
Version
What version(s) of librespot does this problem exist in? All
How to reproduce
Steps to reproduce the behavior in librespot e.g.
- Navigate to open.spotify.com/get_access_token as described by https://github.com/librespot-org/librespot/wiki/Options#access-token
- View error.
Log
Error Spotify gives:
{
"error": {
"code": 400,
"message": "Invalid TOTP"
}
}
Host (what you are running librespot on):
NA
Additional context
This really sucks for a lot of small OSS projects that depend on this for getting anonymous tokens aswell...
That does suck. It was very useful for those projects that got screwed over by the recent API changes. Let's see if we can circumvent this check. It's always going to be cat and mouse with these guys.
I don't use this feature. But the error message "Invalid TOTP" makes me wonder whether it's failing for you because you were using MFA, and whether it was simply an auth failure lack of a "time based one time password" (TOTP)?
No, they've changed something on their end to prevent public access. It was previously a super easy way to get an access token with lots (full?) permissions. It only required you have a couple of browser cookies set (done for you when you login to Spotify.com). It was particularly useful since that included access to all the recently deprecated endpoints. They've now closed that loophole as part of their ongoing effort to lose as many customers as possible.
I assume this endpoint is being used by their developer console, worth having a look there to see if we can undo this new protection.
This error also happened at Spotube, a custom Spotify player, and they found a solution.
It seems Spotify changed how the token thingy works. Now, it is required to have a new request to get the final access token. From the current request, you have to create a totp with a timestamp given by Spotify and request this access_token to then use it on the new clienttoken api request.
See:
- generateTotp from Spotube: https://github.com/KRTirtho/spotube/blob/59f298a935c87077a6abd50656f8a4ead44bd979/lib/provider/authentication/authentication.dart#L135
- generate credentials cookie: https://github.com/KRTirtho/spotube/blob/59f298a935c87077a6abd50656f8a4ead44bd979/lib/provider/authentication/authentication.dart#L184
Something similar like this would need to be implemented here in order to make Spotify integration work again.
Great! For the record, this is just one way to get an access token. We list 2 others on our wiki
As others have noticed in the related issues on other projects, Spotify is changing this a lot, when I first made this issue, it returned the error in my original post. Then SpotAPI cracked the TOTP:
https://github.com/Aran404/SpotAPI/commit/9061bdd53bbfc4b983394593bad6b7d4464245ed
And we were able to get in and it gave this:
{
"clientId": "CLIENTID",
"accessToken": "ACCESSTOKEN",
"accessTokenExpirationTimestampMs": TIMESTAMP,
"isAnonymous": true,
"totpValidity": -1,
"_notes": "Usage of this endpoint is not permitted under the Spotify Developer Terms and Developer Policy, and applicable law"
}
Then a few hours after that commit they openned it up completely and with nothing needed, no params, just like originally it would give this:
{
"clientId": "CLIENTID",
"accessToken": "ACCESSTOKEN",
"accessTokenExpirationTimestampMs": TIMESTAMP,
"isAnonymous": true,
"totpValidity": false,
"_notes": "Usage of this endpoint is not permitted under the Spotify Developer Terms and Developer Policy, and applicable law"
}
Though in my testing Python Requests didn't work code 429? Idk why but http.client worked.
Now it looks like they've changed it once again, opening it with no params returns:
{
"error": {
"code": 400,
"message": "Unauthorized request",
"extra": {
"_notes": "Usage of this endpoint is not permitted under the Spotify Developer Terms and Developer Policy, and applicable law"
}
}
}
While SpotAPIs workaround still works the same, just wanted to document this here, it's very interesting to see what Spotify will decide on.
Welp, that note is something to take seriously.
Also, sometime between all that, (I'm assuming this is related, at least evidence of Spotify changing stuff) Spotify added TOTP login even for non MFA accounts on the web player, if you try logging in now, it'll send a code to your email, to login. It still gives a password login option though.
Thank god for y'all. Needed this badly to fix user search functionality in my app that became broke by their changes :)
Great! For the record, this is just one way to get an access token. We list 2 others on our wiki
Is there a link for it?
If you can't find it: https://github.com/librespot-org/librespot/wiki/Options#access-token
looks like Spotify switched over to HMAC-based One-Time Password for authentication in their latest web player
~~there is no point retrieving server time anymore~~
@sam9116 I do not know how cryptography works. Does this mean we should hold any hope in being able to work around Spotify's new login process? Or should we give up? I really appreciate you checking into how their web player works, they broke my app and I'm in desperate need of a fix :-)
@sam9116 I do not know how cryptography works. Does this mean we should hold any hope in being able to work around Spotify's new login process? Or should we give up? I really appreciate you checking into how their web player works, they broke my app and I'm in desperate need of a fix :-)
as far as I can see, the secrets are still being generated the same way. but you will need to implement a persistent counter that keeps track of how many times the OTP have been requested, since Spotify will keep track of that on their server, if the two numbers doesn't match => OTP incorrect => no token
I'm not sure how sTime and cTime are generated or why they are needed as part of the token request, will have to do more testing around that
interesting, it looks like the counter is actually implemented on the client side
ok, I think I figured out how to get a legit access token from spotify again the new spotify token url parameter is completed as follows
https://open.spotify.com/api/token?reason=init&productType=web-player&totp={ hotp }&totpServer={ hotp }&totpVer=5&sTime={ serverTimeStamp }&cTime={ timestamp }&buildVer={"web-player_2025-06-10_1749524883369_eef30f4"}&buildDate={"2025-06-10"}
otp is computed using the same secret byte array as discussed earlier in this issue
but instead of a totp, it is now an hotp
and you will need to supply a counter
counter is computed using your current time (unix timestamp, in seconds) divided by 30, and then floored
it is still SHA1 hash algorithm, 6 digits,
totp and server totp will be the same value
severTimeStamp will be obtained from https://open.spotify.com/api/server-time, no credential or cookie required, you can reach this from an incognito browser
timestamp will be your current timestamp
buildVer and buildDate will be included in the spotify web player javascript file, you just need to search for buildVer and buildDate in the code
@sam9116 Thank you for trying to figure this out. I still got a 400 error with that ominous note, but the note itself is also present in the response received by spotify web so I guess it's fine. However, I noticed another parameter totpValidUntil which pointed to a date in the past. Any clue on how that works/ if it's required? My current code looks like so :
def get_spotify_access_token(sp_dc_cookie_value):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
}
#raw secret byte array
raw_secret = bytes([12, 56, 76, 33, 88, 44, 88, 33, 78, 78, 11, 66, 22, 22, 55, 69, 54])
b32_secret = base64.b32encode(raw_secret).decode('utf-8')
hotp = pyotp.HOTP(b32_secret)
#HOTP code
now_sec = int(time.time())
counter = now_sec // 30
code = hotp.at(counter)
server_resp = requests.get("https://open.spotify.com/api/server-time", headers=headers)
server_ts = server_resp.json()['serverTime']
url = (
"https://open.spotify.com/api/token?"
f"reason=init&productType=web-player"
f"&totp={code}&totpServer={code}"
f"&totpVer=5"
f"&sTime={server_ts}&cTime={now_sec*1000}"
"&buildVer=unknown"
"&buildDate=unknown"
"&totpValidUntil=Wed Jun 16 2025 20:34:15 GMT+0530 (India Standard Time)"
)
session = requests.Session()
session.headers.update(headers)
session.cookies.set("sp_dc", sp_dc_cookie_value, domain=".spotify.com")
resp = session.get(url)
if resp.ok:
token = resp.json().get("accessToken")
print("Access Token:", token)
return token
else:
print("❌ Failed:", resp.status_code, resp.text)
return None
@Optimuspime123
I don't think the totpValidUntil matters, I just tried it with my old implementation and it still works
@sam9116 I see, would you mind sharing which cookies (other than sp_dc ) you are using, if any? I added an user agent, too - but still get a 400. Shouldn't really be a language specific thing.
@Optimuspime123 I use the entire cookie string provided by the spotify webclient
Thanks, I got it working too :)
Spotify has updated their TOTP secret used for the open.spotify.com/get_access_token endpoint (now open.spotify.com/api/token). The "Invalid TOTP" error occurs because the hardcoded secret in most implementations is outdated.
What Changed?
Spotify rotates their TOTP secrets periodically for security. I've observed that their implementation maintains an array of versioned secrets, and version 5 has been removed from their current rotation. When everything was working previously, version 5 was still available in their secret array. Now they've moved to newer versions, with version 8 being the current active secret.
{
"validUntil": "2025-07-02T12:00:00.000Z",
"secrets": [
{
"secret": [37, 84, 32, 76, 87, 90, 87, 47, 13, 75, 48, 54, 44, 28, 19, 21, 22],
"version": 8
},
{
"secret": [59, 91, 66, 74, 30, 66, 74, 38, 46, 50, 72, 61, 44, 71, 86, 39, 89],
"version": 7
},
{
"secret": [21, 24, 85, 46, 48, 35, 33, 8, 11, 63, 76, 12, 55, 77, 14, 7, 54],
"version": 6
}
]
}
The current secret array in Spotify's JavaScript contains versions 6, 7, and 8, but the old implementations were still using the old version 5 secret, which is why they suddenly stopped working.
Solution
The updated secret cipher bytes for version 8 are:
[37, 84, 32, 76, 87, 90, 87, 47, 13, 75, 48, 54, 44, 28, 19, 21, 22]
If you're using the old version 5 secret:
[12, 56, 76, 33, 88, 44, 88, 33, 78, 78, 11, 66, 22, 22, 55, 69, 54]
You need to update it to the new version 8 values above.
I've successfully tested this fix and can confirm it generates valid TOTP tokens that work with Spotify's current API.
They are going to keep doing this, right? And it's no problem that helpful people (thank you!) keep updating the fixes, but is there any thoughts about popping this in a separate, central project? Or a specific sub project under librespot with its own readme etc. I'm not concerned with extra noise here, it just seems like less people will find it in an issue here given it's only tangentially related to librespot. And it seems worth documenting what's going on with it also.
They are going to keep doing this, right? And it's no problem that helpful people (thank you!) keep updating the fixes, but is there any thoughts about popping this in a separate, central project? Or a specific sub project under librespot with its own readme etc. I'm not concerned with extra noise here, it just seems like less people will find it in an issue here given it's only tangentially related to librespot. And it seems worth documenting what's going on with it also.
sure, if you are willing to create a new project/discussion for this and redirect-people over there.
I've setup a cron job to monitor the Spotify token acquisition process nightly, if it encounters problems, right now an email will be sent out to me, if a new point of discussion is set up I can set up some sort of automation via GitHub and notify everyone subscribed to it
Initially, the process relied on a v8 secret stored in a hardcoded JSON object. As we saw, that JSON had a validUntil field, and right on schedule, Spotify updated the logic.
The next version I analyzed, v9, moved away from the JSON object to a heavily obfuscated string value for its secret. The derivation logic was also new, requiring a multi step process: it took the initial string, performed an XOR cipher on its character codes, converted those values back into a new intermediate string, and then UTF8 encoded that string to get the final bytes for the Base32 key.
The biggest challenge is that halfway through my reverse engineering of that v9 system, they appear to have pushed another significant update. The code has been completely refactored, making the old function names and structures obsolete.
Here are my latest findings from the newest code, based on tracing the live network requests:
-
New Function Names: The call stack now points to a master function called
it(e), which in turn calls a parameter generator function,nt(...), to get the values for the API call. -
Dual TOTP Generation Confirmed: The most important discovery is that there are two separate TOTP values required:
totpandtotpServer. -
Timestamp Logic: My debugging shows
totpis generated using the client's local clock (Date.now()), whiletotpServeris generated using a timestamp fetched from thehttps://open.spotify.com/api/server-timeendpoint. There is also a fallback mechanism: if the call to get the server time fails, the code just uses thetotpvalue for both parameters.
Despite reimplementing this exact logic (deriving the secret and generating two distinct TOTPs with their respective timestamps), my requests are still being rejected with an "Unauthorized request" error. This suggests the secret derivation is even more complex than I've mapped out, or there's another subtle detail I am missing.
Posting my findings here in case it helps anyone else looking into this or if a fresh pair of eyes can spot something I've overlooked. I'll keep at it.
I managed to find the new secret definitions. They've gotten clever and have split the version 9 secret string into three parts to prevent searching for it directly in the source. The string is only reconstructed at runtime.
Here is a snippet showing the definitions for the three most recent versions that I found in the latest code:
// This is where they define the new v9 secret object (obfuscated as 'Ue').
// Notice the secret is concatenated from three parts.
Ue[Le(794, 0, 0, 785)] = Me(585, 569, 584, 578) + Me(569, 586, 573, 580) + "9+$QaH5)N8",
Ue[Le(788, 0, 0, 790)] = 9;
// The v8 secret object for comparison (obfuscated as 'Be').
const Be = {};
Be[Me(0, 586, 0, 591)] = [37, 84, 32, 76, 87, 90, 87, 47, 13, 75, 48, 54, 44, 28, 19, 21, 22],
Be.version = 8;
// The v7 secret is now hardcoded directly in an object literal.
const Fe = {
secret: [59, 91, 66, 74, 30, 66, 74, 38, 46, 50, 72, 61, 44, 71, 86, 39, 89],
version: 7
};
I was able to inspect the final Ue object and can confirm the secret string is the same as the initial v9 version that I was reversing (before they pushed another update):
{
"secret": "meZcB\\tlUFV1D6W2Hy4@9+$QaH5)N8",
"version": 9
}
So, the good news is that the secret string and the derivation logic (XOR -> new string -> UTF8 bytes -> Base32) seem to be confirmed.
The bad news is that my implementation using this key and the dual timestamp logic (local time for totp, server time for totpServer) is still resulting in an "Unauthorized request" error.
I wanted to share this in case it helps anyone else, or if someone spots a flaw in my logic. I'm going to try comparing the final TOTP values generated by my code directly against the ones generated by the browser's JS to see if they differ. I will post another update if I find anything.
They still condense everything into one object after all of the logic:
Ve[Me(0, 597, 0, 594)] = "2025-07-04" + Le(772, 0, 0, 780) + Le(778, 0, 0, 773),
Ve[Me(0, 585, 0, 587)] = [Ue, Be, Fe];
const He = Ve;
The object Ve has the exact same format as the old hardcoded JSON string:
{
"validUntil": "2025-07-04T13:00:00.000Z",
"secrets": [
{
"secret": "meZcB\\tlUFV1D6W2Hy4@9+$QaH5)N8",
"version": 9
},
{
"secret": [37,84,32,76,87,90,87,47,13,75,48,54,44,28,19,21,22],
"version": 8
},
{
"secret": [59,91,66,74,30,66,74,38,46,50,72,61,44,71,86,39,89]
"version": 7
}
]
}
This proves that all the complex, obfuscated code: the string splitting, the function wrappers, is a sophisticated obfuscation layer designed only to build this final object at runtime.
I have successfully reverse-engineered the entire parameter generation flow for the /api/token endpoint in the latest version of the web player.
Here's what I can confirm:
- I can reliably find the master function that calls the parameter generator.
- I can prove with the debugger that the function generates two distinct TOTP values:
totp(fromDate.now()) andtotpServer(from the/api/server-timeendpoint), and it correctly falls back to using thetotpvalue for both if the server time call fails. - By calling their internal function from the console, I can get a valid parameter object that works perfectly in a
curlrequest. This proves the entire logical flow is understood.
I was able to intercept the secret object right before it's used by their internal TOTP generator. This is the final state of the data after all the string splitting, XORing, and other derivations have been completed. The object contains an array of bytes.
{"secret":{"bytes":{"0":49,"1":48,"2":48,"3":49,"4":49,"5":49,"6":56,"7":49,"8":49,"9":49,"10":49,"11":55,"12":57,"13":56,"14":50,"15":49,"16":50,"17":51,"18":49,"19":50,"20":52,"21":54,"22":56,"23":56,"24":52,"25":54,"26":57,"27":51,"28":55,"29":56,"30":49,"31":51,"32":50,"33":54,"34":52,"35":52,"36":50,"37":56,"38":49,"39":57,"40":57,"41":52,"42":55,"43":57,"44":50,"45":51,"46":54,"47":53,"48":51,"49":53,"50":57,"51":49,"52":49,"53":51,"54":54,"55":52,"56":49,"57":48,"58":54,"59":50,"60":50,"61":49,"62":51,"63":49,"64":48,"65":55,"66":51,"67":48}},"version":9}
that's the secret for totp version 10:
byte_list = 0: 53, 1: 50, 2: 49, 3: 48, 4: 48, 5: 52, 6: 57, 7: 49, 8: 49, 9: 48, 10: 52, 11: 54, 12: 54, 13: 53, 14: 49, 15: 50, 16: 50, 17: 56, 18: 53, 19: 51, 20: 49, 21: 57, 22: 57, 23: 48, 24: 55, 25: 57, 26: 49, 27: 49, 28: 52, 29: 56, 30: 48, 31: 55, 32: 53, 33: 54, 34: 50, 35: 49, 36: 50, 37: 53, 38: 53, 39: 49, 40: 56, 41: 49
as a python variable: secret=b"535049484852574949485254545349505056534949575748555749495256485553545049505353495649"
{
"validUntil": "2025-07-07T09:00:00.000Z",
"secrets": [
{
"secret": "=n:b#OuEfH\\fE])e*K",
"version": 10
},
{
"secret": "meZcB\\tlUFV1D6W2Hy4@9+$QaH5)N8",
"version": 9
},
{
"secret": [
37,
84,
32,
76,
87,
90,
87,
47,
13,
75,
48,
54,
44,
28,
19,
21,
22
],
"version": 8
}
]
}