Files
Bose-SoundTouch/docs/reference/spotify-account-addition.md
T
aa7b2c28ab feat: improve Bose SoundTouch parity, Spotify integration, and data reliability (#138)
feat: improve Bose SoundTouch parity, Spotify integration, and data
reliability

- Update XML marshaling for ServicePreset and ServiceRecent to match
Bose parity requirements.
- Add support for adding music sources via
`/streaming/account/{account}/source`.
- Implement HandleBoseAccountToken for Spotify OAuth code exchange and
token persistence.
- Implement atomic file writes in the datastore to prevent data
corruption.
- Add startup logic to initialize default sources for existing devices.
- Expand test coverage with new parity regression and Spotify
integration tests.

---------

Co-authored-by: Junie <junie@jetbrains.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-04-01 21:55:52 +02:00

7.5 KiB

Spotify Account Addition Technical Reference

This document details the exact network requests performed by the Bose SoundTouch "Stockholm" application and the SoundTouch speaker when adding a new Spotify account. This information is based on analysis of the Stockholm firmware version 27.0.13-4277-8963611.

Flow Overview

  1. User Authorization Initiation: The app opens the system browser to Spotify's authorization page.
  2. Redirect Handling: After authorization, Spotify redirects back to the app via a custom URI scheme, delivering an authorization code.
  3. OAuth Token Exchange: The app sends this code to the background worker, which exchanges it for a Bose-mediated token.
  4. Cloud Source Registration: The app registers the Spotify account as a "source" in the user's Bose Cloud (Marge) profile.
  5. Local Device Sync: The app notifies the local SoundTouch speaker about the new source, which then updates its internal configuration.

0. User Authorization Initiation

The process begins in the Stockholm UI when the user selects Spotify to add a new account.

Request Details (App to Browser)

  • Action: Open System Browser
  • Base URL: [SPOTIFY_AUTH_URL] (e.g., https://accounts.spotify.com/authorize)
  • Query Parameters:
    • client_id: Bose Spotify Client ID
    • response_type: code
    • redirect_uri: http://localhost (often used as a placeholder or specifically handled by the app's internal webview/proxy)
    • scope: user-read-private user-read-email ...
    • state: A base64-encoded JSON object containing metadata, e.g., {"service": "SPOTIFY"}.

Redirect (Browser to App)

Upon successful login and authorization, Spotify redirects the browser to a URL that the SoundTouch app intercepts.

  • URL Format: soundtouch://bose/musicservice/spotify/login?code=[AUTH_CODE]&state=[STATE]
  • App Action: The UIMain component (in ui_main.js) handles this "deep link". It extracts the code from the query parameters and prepares to send it to the background worker.

1. OAuth Token Exchange (Bose Cloud)

After the UI intercepts the redirect and extracts the code, it sends a createOAuthAccountRequest to the background SpotifyWorker. The worker then performs the exchange for a Bose-mediated token.

What is a "Bose-mediated token"?

The "Bose-mediated token" is a token issued by the Bose OAuth proxy. When the app (or device) requests a token via oauth.streaming.bose.com, Bose's service performs the actual OAuth2 exchange with Spotify.

  • It is not directly a Spotify refresh token: Instead, it is a Bose-issued token that represents the underlying Spotify session.
  • Token Version 3: Modern firmware uses token_version_3, which signifies that the device doesn't store the raw Spotify tokens but instead uses a Bose-specific "secret" that the Bose Cloud uses to fetch fresh Spotify access tokens on the device's behalf.
  • Access vs Refresh: The initial response from the .../token/cs endpoint typically contains an access_token (valid for ~1 hour) and a token_type: "Bearer". The Bose cloud service manages the persistent refresh token internally.

Internal Message (UI to Worker)

  • Message Type: createOAuthAccountRequest
  • Payload:
    {
      "source": "SPOTIFY",
      "code": "[AUTH_CODE_FROM_REDIRECT]",
      "credentialType": "token_version_3"
    }
    

Outgoing Request (Worker to Bose OAuth Proxy)

  • Endpoint: https://oauth.streaming.bose.com/oauth/account/[ACCOUNT_ID]/music/musicprovider/15/token/cs
  • Method: POST
  • Headers:
    • Content-Type: application/json
    • Accept: application/json
    • Authorization: Bearer [SESSION_TOKEN] (The user's Bose account session token)

Payload (JSON)

{
  "grant_type": "authorization_code",
  "code": "[AUTH_CODE_FROM_SPOTIFY]",
  "redirect_uri": "http://localhost"
}

curl Example

curl -X POST "https://oauth.streaming.bose.com/oauth/account/[ACCOUNT_ID]/music/musicprovider/15/token/cs" \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer [SESSION_TOKEN]" \
     -d '{
       "grant_type": "authorization_code",
       "code": "[AUTH_CODE_FROM_SPOTIFY]",
       "redirect_uri": "http://localhost"
     }'

2. Cloud Source Registration (Marge)

The app now registers the Spotify account with the Bose "Marge" service. This makes the source available across all devices linked to the same Bose account.

Request Details

  • Endpoint: https://streaming.bose.com/streaming/account/[ACCOUNT_ID]/source
  • Method: POST
  • Headers:
    • Content-Type: application/vnd.bose.streaming-v1.1+xml
    • Authorization: [MARGE_TOKEN]
    • GUID: [DEVICE_GUID]
    • ClientType: Stockholm

Payload (XML)

<?xml version="1.0" encoding="UTF-8"?>
<source>
    <username>[SPOTIFY_USER_ID]</username>
    <sourceproviderid>15</sourceproviderid>
    <credential type="token_version_3">[SECRET_TOKEN_OBTAINED_IN_STEP_1]</credential>
    <sourcename>[DISPLAY_NAME_E_G_EMAIL]</sourcename>
</source>

curl Example

curl -X POST "https://streaming.bose.com/streaming/account/[ACCOUNT_ID]/source" \
     -H "Content-Type: application/vnd.bose.streaming-v1.1+xml" \
     -H "Authorization: [MARGE_TOKEN]" \
     -d '<?xml version="1.0" encoding="UTF-8"?><source><username>[USER]</username><sourceproviderid>15</sourceproviderid><credential type="token_version_3">[TOKEN]</credential><sourcename>[NAME]</sourcename></source>'

3. Local Device Notification (LISA API)

The app notifies the physical SoundTouch speaker about the new source. This is usually done via the device's management API on port 8090.

Request Details

  • Endpoint: http://[DEVICE_IP]:8090/setMusicServiceOAuthAccount
  • Method: POST

Payload (XML)

<OAuthCredentials source="SPOTIFY" displayName="[DISPLAY_NAME]">
    <user>[SPOTIFY_USER_ID]</user>
    <code>[AUTH_CODE_OR_TOKEN]</code>
    <version>token_version_3</version>
</OAuthCredentials>

Note: In some cases, the app sends a wrapped message format if communicating over WebSockets:

<msg>
    <header deviceID="[DEVICE_UID]" url="setMusicServiceOAuthAccount" method="POST">
        <request requestID="1">
            <info type="new" />
            <sourceItem source="SPOTIFY" />
        </request>
    </header>
    <body>
        <OAuthCredentials source="SPOTIFY" displayName="[NAME]">
            <user>[USER]</user>
            <code>[TOKEN]</code>
            <version>token_version_3</version>
        </OAuthCredentials>
    </body>
</msg>

Placeholders and Constants

Placeholder Description
[ACCOUNT_ID] The internal Bose account ID (UUID).
[SESSION_TOKEN] Temporary token from Bose login.
[MARGE_TOKEN] Persistent authorization token for Marge services.
[DEVICE_GUID] Unique identifier for the controller app instance.
[DEVICE_IP] Local IP address of the SoundTouch speaker.
15 Constant sourceproviderid for Spotify.
token_version_3 Credential type for modern OAuth2 Spotify accounts.

Resulting Persistence

Once these requests succeed, the device updates its /mnt/nv/BoseApp-Persistence/1/Sources.xml file:

<source displayName="user@example.com" secret="[SECRET_BLOB]" secretType="token_version_3">
    <sourceKey type="SPOTIFY" account="user" />
</source>