# SoundTouch Device WebSocket API — Pairing & Operation Flow
Reference document derived from mitmproxy captures of the Bose SoundTouch Android app
(`bose-pairing-20260502-155542`, `bose-pairing-20260502-165549`).
> **Why this matters:** With Bose cloud services shutting down on 2026-05-06, the original
> app may stop working for pairing and playback control. This document captures the exact
> WebSocket message sequences needed to replicate those flows independently.
---
## Connection
All interactions use the SoundTouch WebSocket API on the speaker's local IP, port **8090**
(the same port as the REST API). Connect with the `Gabbo` sub-protocol:
```
GET ws://192.168.x.y:8090/
Upgrade: websocket
Sec-WebSocket-Protocol: Gabbo
```
Upon connection the server immediately sends an identification banner:
```xml
```
---
## Message Envelope
All subsequent messages (except `selectLastWiFiSource`, see below) use this envelope:
**Client → Server request:**
```xml
```
**Server → Client response:**
```xml
```
**Server → Client push (unsolicited):**
```xml
...
```
`requestID` is a monotonically increasing integer per connection (client-side sequence).
`{device_id}` is the speaker's MAC address with colons removed (e.g. `08DF1F0BA325`).
---
## Phase 1 — Discovery: Is the Speaker Already Paired?
```xml
SoundTouch 10
SoundTouch 10
9569497
https://streaming.bose.com
...
```
- **Empty `margeAccountUUID`** → device is unpaired, proceed to Phase 2
- **Populated `margeAccountUUID`** → already paired with that account ID
---
## Phase 2 — Pairing a New Speaker
### 2.1 Setup State Machine
The pairing flow uses a setup state machine on the device. States must be sent in order.
```xml
3
My SoundTouch 10
```
### 2.2 Account Pairing — The Critical Step
```xml
{accountId}
Bearer {token}
...
{accountId}
...
```
The server also pushes several `sourcesUpdated` events after successful pairing.
**`{accountId}`** — the numeric Bose account ID (e.g. `9569497`), obtainable from
`GET /streaming/account/login` on soundtouch-service.
**`{token}`** — a Bearer token issued by Bose authentication (or soundtouch-service).
The full token from the captures:
```
Bearer NtJDRbNtY3hDhm5K8FC2JprRhRQNH3QdZjG6aR4ASwYQg4rvZMY6dPLc3Bm6zvWNciWzCpMWZ/dbITRQoVdClOdssgDO+Nlh4ZJWp2w3tZiGzB8Flho0c+ipXnT/0Yg5
```
(session-specific; obtain a fresh one from the service's account login flow)
### 2.3 Finish Setup and Telemetry
```xml
/pushCustomerSupportInfoToMarge
```
---
## Phase 3 — Unpairing
```xml
```
---
## Phase 4 — App Initialization (Bulk State Fetch)
When the app connects to an already-paired device it sends these in rapid parallel sequence:
```
info (GET) — device metadata, check pairing
sources (GET) — available input sources
presets (GET) — saved presets 1–6
swUpdateQuery (POST) — check if update is in progress
capabilities (GET) — hardware capabilities, network config
bassCapabilities (GET) — bass range and defaults
now_playing (GET) — current playback state
volume (GET) — current volume
getZone (GET) — multi-room zone membership
clockDisplay (POST) — set clock timezone/format
```
Then a second wave:
```
swUpdateCheck (POST) — check for new firmware
systemtimeout (GET) — power-saving timeout
rebroadcastlatencymode (GET) — zone latency mode
getGroup (GET) — stereo-pair group
language (GET, sourceItem source="settings") — UI language
bass (GET) — current bass level
serviceAvailability (GET, sourceItem source="add_service" or "settings")
webserver/pingRequest (GET) — keepalive
pushCustomerSupportInfoToMarge (GET) — telemetry
netStats (GET, sourceItem source="settings") — network statistics
introspect (POST, sourceItem source="AIRPLAY") — AirPlay2 capabilities
```
`clockDisplay` example with timezone:
```xml
```
`serviceAvailability` response lists availability of all service types (PANDORA, AIRPLAY,
AMAZON, DEEZER, SPOTIFY, TUNEIN, SIRIUSXM_EVEREST, BLUETOOTH, etc.) with `isAvailable`
and optional `reason` attributes.
---
## Playback Control
### Start Playback via `playbackRequest` (preferred — bypasses source checks)
```xml
1LIVE
```
For a TuneIn podcast episode, use `type="tracklisturl"` and
`location="/v1/playback/episodes/{id}?encoded_name={base64}"`.
### Select Content via `select` (triggers preset/recents UI highlight)
```xml
1LIVE
```
Note: `select` with a TUNEIN item that the device can't resolve directly may return
`error value="1005" name="UNKNOWN_SOURCE_ERROR"`. Use `playbackRequest` instead for
reliable playback.
### Special: Select Last Wi-Fi Source
A plain-text (non-XML) client message:
```
selectLastWiFiSource
```
Server responds with plain text:
```
/selectLastWiFiSource
```
### Key Presses
```xml
{KEY}
{KEY}
```
Key names observed: `POWER`, `STOP`, `PAUSE`, `ADD_FAVORITE`
`sender="Gabbo"` is the app identifier string used by all Bose mobile apps.
### Volume
```xml
30
3030false
```
### Bass
```xml
-2
```
---
## Browse & Navigate
```xml
4
-
1LIVE
...
```
Use `type="update"` on `` for subsequent refresh calls on the same menu.
---
## Settings
### System Timeout (Power-Saving)
```xml
false
```
### Clock Display
```xml
```
---
## Keepalive
The app sends a ping roughly every 30 seconds:
```xml
```
---
## Server Push Events (Unsolicited)
The server wraps push events in ``:
| Event element | Trigger |
|----------------------------------|-----------------------------------------------------------------------------------------------------|
| `nowPlayingUpdated` | Source/track changed, playback state changed |
| `nowSelectionUpdated` | Preset slot highlighted (UI selection changed) |
| `recentsUpdated` | Recents list changed |
| `presetsUpdated` | Preset saved or modified |
| `volumeUpdated` | Volume changed (any source) |
| `bassUpdated` | Bass level changed |
| `connectionStateUpdated` | Wi-Fi signal strength changed (`EXCELLENT_SIGNAL`, `GOOD_SIGNAL`, `MARGINAL_SIGNAL`, `POOR_SIGNAL`) |
| `soundTouchConfigurationUpdated` | Setup state changed (e.g. `SOUNDTOUCH_CONFIGURING`) |
| `infoUpdated` | Device info changed (e.g. after un-pairing) |
| `sourcesUpdated` | Available sources list changed |
Separate push (not inside ``):
```xml
```
Sent after any physical or app-initiated user action.
---
## Notification (Client → Device Push)
Used by the app to notify the device of data that has changed on the service side
(e.g. after syncing presets from cloud). Header uses `propagate="false"`:
```xml
/notification
```
---
## Complete Pairing Sequence (Minimal)
To pair a freshly factory-reset speaker to a Bose account (soundtouch-service must be
running and authenticated):
```
1. Connect WebSocket to ws://{speakerIP}:8090/
2. Receive:
3. GET info → confirm margeAccountUUID is empty
4. POST setup SETUP_START
5. POST setup SETUP_IDENTIFY_DEVICE_ENTER (timeout=300000)
(user physically presses button on speaker to confirm identity)
6. POST language 3
7. POST setup SETUP_ENTER
8. POST setup SETUP_IDENTIFY_DEVICE_LEAVE
9. POST name {desired name}
10. POST setMargeAccount
{accountId}
Bearer {token}
→ device responds with info, margeAccountUUID is now set
11. POST setup SETUP_LEAVE
12. GET pushCustomerSupportInfoToMarge (telemetry, safe to skip)
```
---
## Source References
- `bose-pairing-20260502-155542` — Session 1: initial pairing of SoundTouch 10 to account 9569497
- `bose-pairing-20260502-165549` — Session 2: re-pairing and full operation (TuneIn, Spotify, presets)
- Raw WebSocket files: `scripts/android/mitm/{session}/mirror/{n}-websocket/*.txt`
- Companion HTTP upgrade files: `scripts/android/mitm/{session}/mirror/{n}-*.http`