@jacquesbeets/capacitor-spotify
v0.4.1
Published
Capacitor plugin for Spotify: native App Remote playback + auth on iOS/Android, Web Playback SDK on web
Maintainers
Readme
capacitor-spotify
Capacitor plugin for Spotify (Capacitor 7 and 8): one TypeScript API over the Spotify iOS SDK (App Remote), the Spotify Android SDK (App Remote + auth library), and the Web Playback SDK.
- iOS / Android — controls playback in the installed Spotify app via App Remote: play/pause/skip/seek/shuffle/repeat, live player-state events, OAuth (PKCE) tokens for your own Web API calls.
- Web — streams inside the browser via the Web Playback SDK (your page becomes a Spotify Connect device).
Platform support
| Feature | iOS | Android | Web |
| --- | :-: | :-: | :-: |
| authorize() / getAccessToken() (OAuth + PKCE, auto-refresh) | ✅ | ✅ | ✅ |
| connect() / disconnect() to player | ✅ | ✅ | ✅ |
| play(uri) / pause / resume / togglePlay | ✅ | ✅ | ✅ |
| skipNext / skipPrevious / seekTo | ✅ | ✅ | ✅ |
| setShuffle / setRepeatMode | ✅ | ✅ | ✅ |
| setVolume / getVolume | ❌ NOT_SUPPORTED | ✅ / best-effort | ✅ |
| getImage (album art) | ✅ via Spotify app | ✅ via Spotify app | ✅ CDN URL |
| getUserCapabilities (Premium check) | ✅ via Spotify app | ✅ via Spotify app | ✅ when connected |
| addToQueue / getDevices / transferPlayback | ✅ Web API | ✅ Web API | ✅ Web API |
| playerStateChanged live events | ✅ | ✅ | ✅ |
| Audio plays… | in the Spotify app | in the Spotify app | in your web page |
| Requires Spotify app installed | ✅ | ✅ | — |
| Requires Spotify Premium | for on-demand URI playback | for on-demand URI playback | ✅ always |
Requirements
- Capacitor 7 or 8 (iOS 14+; Android minSdk 23 on Capacitor 7, 24 on Capacitor 8)
- Your own Spotify app (client ID) from the Spotify Developer Dashboard — see setup below
- iOS/Android: the Spotify app installed on the device, user logged in
- Web: a real browser with DRM (Widevine/FairPlay) support and a Premium account. The Web Playback SDK does not work inside the Capacitor webview — the native implementations exist for exactly that reason. Use
getCapabilities().webPlaybackViableto detect support.
[!IMPORTANT] Spotify Development Mode limits (since Feb/Mar 2026): the account owning your Spotify app must hold an active Premium subscription, and only 5 users (allowlisted in User Management) can use the app. Higher limits require extended quota mode, which Spotify currently grants only to registered organizations with an active service of ≥250k MAU. This is a Spotify platform policy, not a plugin limitation — plan your product accordingly.
Install
npm install @jacquesbeets/capacitor-spotify
npx cap syncOr straight from GitHub (e.g. to try an unreleased commit):
npm install https://github.com/JacquesBeets/capacitor-spotify.git
npx cap syncInstalling from a git URL runs the plugin's build (
prepublishOnly) only when packed. Ifdist/is missing after a git install, runnpm --prefix node_modules/@jacquesbeets/capacitor-spotify run buildonce, or install from a packed tarball (npm packin a plugin checkout →npm install <tarball>).
Spotify Developer Dashboard setup
- Create an app at https://developer.spotify.com/dashboard (accept the Developer Terms). Note your Client ID — you never need the client secret (the plugin uses PKCE; don't ship the secret in an app).
- In Settings → APIs used, enable Web API (and Web Playback SDK if you target web).
- Add Redirect URIs (exact match, per platform):
- Native (iOS/Android): a custom scheme, e.g.
myapp://spotify-callback. All-lowercase; Spotify recommends App Links / Universal Links for production. - Web: an
https://URL of your app. For local dev usehttp://127.0.0.1:<port>/—localhostis rejected.
- Native (iOS/Android): a custom scheme, e.g.
- Android: register your package name and SHA-1 signing fingerprint (both debug and release):
keytool -list -v -alias androiddebugkey -keystore ~/.android/debug.keystore -storepass android | grep SHA1 - iOS: register your bundle ID.
- User Management: add each test user's Spotify account (max 5 in development mode). Non-allowlisted users get
403/USER_NOT_AUTHORIZED.
iOS setup
Add to your app's Info.plist:
<!-- Lets the plugin detect + launch the Spotify app -->
<key>LSApplicationQueriesSchemes</key>
<array>
<string>spotify</string>
</array>
<!-- Your OAuth redirect scheme (the part before ://) -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.yourcompany.yourapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>Notes:
- The plugin handles the redirect callback itself (via Capacitor's URL-open events) — no AppDelegate changes needed.
- The Spotify iOS SDK requires the Spotify app to be actively playing for a plain connect. When it isn't,
connect({ playUri })falls back toauthorizeAndPlayURI, which briefly app-switches to Spotify, starts playback, and returns. PassplayUri: ''to resume the user's last context. - iOS cannot control the Spotify app's volume —
setVolume()/getVolume()reject withNOT_SUPPORTED.
Android setup
Since com.spotify.android:auth 5.0.0 the redirect receiver must be declared in your app's AndroidManifest.xml (inside <application>), with your redirect URI's scheme/host:
<activity
android:name="com.spotify.sdk.android.auth.browser.RedirectUriReceiverActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" android:host="spotify-callback" />
</intent-filter>
</activity>Notes:
- The Spotify App Remote AAR is not on Maven Central; this plugin vendors it and registers a local Maven repository automatically — no Gradle changes needed in your app. (Exception: see Troubleshooting if your app uses
FAIL_ON_PROJECT_REPOS.) - The plugin's manifest already ships the
<queries>entry needed to see the Spotify app on Android 11+.
Web setup
No install steps. At runtime the plugin injects https://sdk.scdn.co/spotify-player.js and creates a Spotify Connect device named after playerName.
- Works in real desktop/mobile browsers with EME/DRM; not in the Capacitor webview.
connect()must be called from a user gesture (click/tap) — browser autoplay policy.- The user needs Spotify Premium (
account_error→PREMIUM_REQUIREDotherwise).
Usage
import { Spotify } from '@jacquesbeets/capacitor-spotify';
// 1. Initialize once at startup. On web this also completes a pending
// OAuth redirect, so call it before anything else.
await Spotify.initialize({
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'myapp://spotify-callback', // or https://... on web
playerName: 'My Awesome App',
});
// 2. Authenticate (interactive; PKCE — tokens are stored & refreshed for you)
const token = await Spotify.authorize();
// ...use the token for your own Web API calls whenever you like:
const { accessToken } = await Spotify.getAccessToken();
const me = await fetch('https://api.spotify.com/v1/me', {
headers: { Authorization: `Bearer ${accessToken}` },
}).then((r) => r.json());
// 3. Listen to events
await Spotify.addListener('playerStateChanged', (state) => {
console.log(state.paused ? 'paused' : 'playing', state.track?.name);
});
await Spotify.addListener('connectionStateChanged', (ev) => {
console.log('connected:', ev.connected, ev.reason ?? '');
});
// 4. Connect (from a user gesture!) and play
await Spotify.connect({ playUri: '' });
await Spotify.play({ uri: 'spotify:playlist:37i9dQZF1DXcBWIGoYBM5M' });
// 5. Control playback
await Spotify.pause();
await Spotify.skipNext();
await Spotify.seekTo({ positionMs: 30_000 });
await Spotify.setRepeatMode({ repeatMode: 'context' });
// 6. Inspect state on demand
const state = await Spotify.getPlayerState();Error handling — every rejection carries a stable code:
try {
await Spotify.connect();
} catch (err: any) {
switch (err.code) {
case 'SPOTIFY_APP_NOT_INSTALLED': /* prompt install */ break;
case 'PREMIUM_REQUIRED': /* explain premium */ break;
case 'NOT_AUTHENTICATED': await Spotify.authorize(); break;
default: console.error(err.code, err.message);
}
}A runnable demo lives in example-app/ — bring your own client ID.
API
initialize(...)authorize(...)getAccessToken(...)logout()isSpotifyAppInstalled()getCapabilities()connect(...)disconnect()isConnected()play(...)pause()resume()togglePlay()skipNext()skipPrevious()seekTo(...)setShuffle(...)setRepeatMode(...)setVolume(...)getVolume()getPlayerState()getImage(...)getUserCapabilities()addToQueue(...)getDevices()transferPlayback(...)addListener('playerStateChanged', ...)addListener('connectionStateChanged', ...)addListener('authStateChanged', ...)removeAllListeners()- Interfaces
- Type Aliases
initialize(...)
initialize(options: InitializeOptions) => Promise<void>Configure the plugin. Must be called before any other method. Idempotent.
Web: also completes a pending OAuth redirect — call it on app startup so
a ?code= callback on the current URL resolves the in-flight
authorize() flow.
| Param | Type |
| ------------- | --------------------------------------------------------------- |
| options | InitializeOptions |
authorize(...)
authorize(options?: AuthorizeOptions | undefined) => Promise<AccessToken>Launch interactive Spotify authorization.
iOS: SPTSessionManager (app-switch to Spotify, or in-app web auth when
Spotify is not installed). Android: Spotify auth library (SSO via the
Spotify app, Custom Tabs fallback). Web: Authorization Code + PKCE via a
full-page redirect.
All platforms use PKCE — no client secret is involved.
| Param | Type |
| ------------- | ------------------------------------------------------------- |
| options | AuthorizeOptions |
Returns: Promise<AccessToken>
getAccessToken(...)
getAccessToken(options?: { forceRefresh?: boolean | undefined; } | undefined) => Promise<AccessToken>Get a valid access token, refreshing it internally when it is about to
expire. Rejects with NOT_AUTHENTICATED when there is no session.
Use this to call the Spotify Web API from your own code.
| Param | Type |
| ------------- | ---------------------------------------- |
| options | { forceRefresh?: boolean; } |
Returns: Promise<AccessToken>
logout()
logout() => Promise<void>Clear the stored session and disconnect the player if connected.
isSpotifyAppInstalled()
isSpotifyAppInstalled() => Promise<{ installed: boolean; }>Whether the Spotify app is installed on this device. Always false on web.
Returns: Promise<{ installed: boolean; }>
getCapabilities()
getCapabilities() => Promise<SpotifyCapabilities>What this platform supports — see {@link SpotifyCapabilities}.
Returns: Promise<SpotifyCapabilities>
connect(...)
connect(options?: ConnectOptions | undefined) => Promise<void>Connect to the player.
iOS/Android: connects to the Spotify app via App Remote and subscribes to player state. Web: loads the Web Playback SDK and creates the Connect device — must be called from a user gesture (tap/click).
| Param | Type |
| ------------- | --------------------------------------------------------- |
| options | ConnectOptions |
disconnect()
disconnect() => Promise<void>Disconnect from the player. Safe to call when not connected.
isConnected()
isConnected() => Promise<{ connected: boolean; }>Returns: Promise<{ connected: boolean; }>
play(...)
play(options?: PlayOptions | undefined) => Promise<void>Play a Spotify URI, or resume playback when no URI is given.
Web: starting a URI requires this SDK device to be (or become) the active device; the plugin transfers playback automatically on first play.
| Param | Type |
| ------------- | --------------------------------------------------- |
| options | PlayOptions |
pause()
pause() => Promise<void>resume()
resume() => Promise<void>togglePlay()
togglePlay() => Promise<void>skipNext()
skipNext() => Promise<void>skipPrevious()
skipPrevious() => Promise<void>seekTo(...)
seekTo(options: { positionMs: number; }) => Promise<void>| Param | Type |
| ------------- | ------------------------------------ |
| options | { positionMs: number; } |
setShuffle(...)
setShuffle(options: { enabled: boolean; }) => Promise<void>| Param | Type |
| ------------- | ---------------------------------- |
| options | { enabled: boolean; } |
setRepeatMode(...)
setRepeatMode(options: { repeatMode: RepeatMode; }) => Promise<void>| Param | Type |
| ------------- | ------------------------------------------------------------------ |
| options | { repeatMode: RepeatMode; } |
setVolume(...)
setVolume(options: { volume: number; }) => Promise<void>Set player volume (0.0–1.0). Android and web only — iOS rejects with
NOT_SUPPORTED (the Spotify iOS SDK has no volume control).
| Param | Type |
| ------------- | -------------------------------- |
| options | { volume: number; } |
getVolume()
getVolume() => Promise<{ volume: number; }>Get player volume (0.0–1.0). Web and Android (best-effort) — iOS rejects
with NOT_SUPPORTED.
Returns: Promise<{ volume: number; }>
getPlayerState()
getPlayerState() => Promise<PlayerState>One-shot player state snapshot. Rejects NOT_CONNECTED when the player
is not connected, or NOT_ACTIVE_DEVICE on web when playback lives on
another device.
Returns: Promise<PlayerState>
getImage(...)
getImage(options: GetImageOptions) => Promise<GetImageResult>Fetch album art for a track. Pass {@link Track.imageUri} from a player
state as imageId. iOS/Android fetch through the Spotify app (works with
its offline cache) and require a connected player; web resolves to a CDN
URL without a network round-trip.
| Param | Type |
| ------------- | ----------------------------------------------------------- |
| options | GetImageOptions |
Returns: Promise<GetImageResult>
getUserCapabilities()
getUserCapabilities() => Promise<UserCapabilities>Whether the account can play content on demand (Premium) — check before
enabling seek/track-pick UI. iOS/Android read this from the Spotify app
(requires a connected player). Web: true once the player is connected
(the Web Playback SDK itself requires Premium); when not connected it is
inferred from the user profile where available and otherwise rejects
NOT_SUPPORTED (development-mode apps get no subscription field).
Returns: Promise<UserCapabilities>
addToQueue(...)
addToQueue(options: { uri: string; }) => Promise<void>Append a track/episode URI to the playback queue (Web API on all platforms; requires an active device and Premium).
| Param | Type |
| ------------- | ----------------------------- |
| options | { uri: string; } |
getDevices()
getDevices() => Promise<{ devices: SpotifyDevice[]; }>List the user's available Spotify Connect devices (Web API).
Returns: Promise<{ devices: SpotifyDevice[]; }>
transferPlayback(...)
transferPlayback(options: { deviceId: string; play?: boolean; }) => Promise<void>Transfer playback to another Connect device (Web API). With
play: true playback starts on the target immediately.
| Param | Type |
| ------------- | -------------------------------------------------- |
| options | { deviceId: string; play?: boolean; } |
addListener('playerStateChanged', ...)
addListener(eventName: 'playerStateChanged', listener: (state: PlayerState) => void) => Promise<PluginListenerHandle>Fired whenever the player state changes (track, pause, seek, ...).
| Param | Type |
| --------------- | ----------------------------------------------------------------------- |
| eventName | 'playerStateChanged' |
| listener | (state: PlayerState) => void |
Returns: Promise<PluginListenerHandle>
addListener('connectionStateChanged', ...)
addListener(eventName: 'connectionStateChanged', listener: (event: ConnectionStateChange) => void) => Promise<PluginListenerHandle>Fired when the player connection is established or lost.
| Param | Type |
| --------------- | ------------------------------------------------------------------------------------------- |
| eventName | 'connectionStateChanged' |
| listener | (event: ConnectionStateChange) => void |
Returns: Promise<PluginListenerHandle>
addListener('authStateChanged', ...)
addListener(eventName: 'authStateChanged', listener: (event: AuthStateChange) => void) => Promise<PluginListenerHandle>Fired when authentication is gained, refreshed, or lost.
| Param | Type |
| --------------- | ------------------------------------------------------------------------------- |
| eventName | 'authStateChanged' |
| listener | (event: AuthStateChange) => void |
Returns: Promise<PluginListenerHandle>
removeAllListeners()
removeAllListeners() => Promise<void>Interfaces
InitializeOptions
| Prop | Type | Description | Default |
| --------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| clientId | string | Your Spotify app's client ID from the Spotify Developer Dashboard. Every consumer of this plugin must register their own Spotify app. | |
| redirectUri | string | OAuth redirect URI. Must exactly match a Redirect URI registered in the Spotify Developer Dashboard. Native: a custom scheme such as myapp://spotify-callback. Web: an https:// page of your app (or http://127.0.0.1:port in dev — localhost is not allowed by Spotify). | |
| scopes | string[] | OAuth scopes to request during {@link SpotifyPlugin.authorize}. | ["app-remote-control", "streaming", "user-modify-playback-state", "user-read-playback-state", "user-read-currently-playing"] |
| playerName | string | Web only: name of the Spotify Connect device created by the Web Playback SDK, shown in Spotify's device picker. | "Capacitor App" |
| tokenSwapUrl | string | iOS only: URL of your token-swap service. Optional — by default the plugin uses Authorization Code + PKCE and needs no server. | |
| tokenRefreshUrl | string | iOS only: URL of your token-refresh service. Optional — by default the plugin refreshes tokens itself via PKCE. | |
AccessToken
| Prop | Type | Description |
| ----------------- | --------------------- | ------------------------------------------------------------------------------------- |
| accessToken | string | The OAuth access token. Pass as Authorization: Bearer <token> to the Web API. |
| expiresAt | number | Expiry time as epoch milliseconds. |
| scopes | string[] | Granted scopes, when known. |
| tokenType | 'Bearer' | |
AuthorizeOptions
| Prop | Type | Description |
| ---------------- | --------------------- | ------------------------------------------------------------------------ |
| scopes | string[] | Override the scopes given to initialize() for this grant. |
| showDialog | boolean | Web only: force the Spotify approval dialog even if previously approved. |
SpotifyCapabilities
| Prop | Type | Description |
| ------------------------ | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| platform | 'ios' | 'android' | 'web' | |
| requiresSpotifyApp | boolean | True on iOS/Android: the Spotify app must be installed for playback. |
| requiresPremium | boolean | True on web: Spotify Premium is required for the Web Playback SDK. |
| canSetVolume | boolean | True where setVolume() works (Android, web). |
| canGetVolume | boolean | True where getVolume() works (web; Android best-effort). |
| webPlaybackViable | boolean | Web only: whether the browser has the EME/DRM support (Widevine) the Web Playback SDK needs. False in most native webviews — use the native platforms there. Always false on iOS/Android. |
ConnectOptions
| Prop | Type | Description |
| ------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| playUri | string | iOS only: Spotify URI passed to authorizeAndPlayURI when connecting while the Spotify app is not playing (iOS requires active playback to connect — this wakes Spotify up, briefly app-switching to it). Empty string resumes the user's last context. Ignored on Android/web. |
PlayOptions
| Prop | Type | Description |
| --------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| uri | string | A Spotify URI (spotify:track:..., spotify:album:..., spotify:playlist:..., spotify:artist:...). Omit to resume playback. |
PlayerState
| Prop | Type | Description |
| ------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| track | Track | null | Currently playing track, or null when nothing is loaded. |
| paused | boolean | |
| positionMs | number | |
| playbackSpeed | number | Playback speed multiplier. Always 1 on web. |
| shuffle | boolean | |
| repeatMode | RepeatMode | |
| restrictions | PlaybackRestrictions | What the current context allows — drive UI enablement from this. |
| contextUri | string | URI of the playing context (album/playlist/...), when known. |
| contextTitle | string | Title of the playing context. iOS/Android only. |
| receivedAtMs | number | Epoch ms timestamp of when this snapshot was taken — extrapolate the live position as positionMs + (Date.now() - receivedAtMs) while playing. |
Track
| Prop | Type | Description |
| ---------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| uri | string | |
| name | string | |
| artistName | string | Convenience: name of the primary artist. |
| artists | Artist[] | |
| albumName | string | |
| albumUri | string | |
| durationMs | number | |
| imageUri | string | Web: an https image URL. iOS/Android: a raw Spotify image identifier (not directly loadable; image fetch support is planned future work). |
| isEpisode | boolean | |
| isPodcast | boolean | |
Artist
| Prop | Type |
| ---------- | ------------------- |
| name | string |
| uri | string |
PlaybackRestrictions
| Prop | Type |
| ---------------------- | -------------------- |
| canSkipNext | boolean |
| canSkipPrevious | boolean |
| canSeek | boolean |
| canToggleShuffle | boolean |
| canRepeatTrack | boolean |
| canRepeatContext | boolean |
GetImageResult
| Prop | Type | Description |
| ------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| dataUrl | string | A value directly usable as an <img src>: a base64 data: URI on iOS/Android (fetched through the Spotify app), an https:// URL on web. |
GetImageOptions
| Prop | Type | Description | Default |
| ------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------- |
| imageId | string | The image identifier from {@link Track.imageUri} — a spotify:image:... value on iOS/Android, or an https:// URL on web. | |
| width | number | Desired image width in pixels. Native maps this to the nearest size the Spotify app provides (144/240/360/480/720); web ignores it. | 480 |
UserCapabilities
| Prop | Type | Description |
| --------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| canPlayOnDemand | boolean | Whether the user's account can play arbitrary content on demand (Premium). Free-tier accounts get shuffle-based playback and cannot seek or pick exact tracks — check this before enabling such UI. |
SpotifyDevice
| Prop | Type | Description |
| ---------------------- | --------------------------- | -------------------------------------------------------------------------- |
| id | string | null | Connect device ID. Null for devices that cannot be targeted. |
| name | string | |
| type | string | Device kind reported by Spotify, e.g. Computer, Smartphone, Speaker. |
| isActive | boolean | |
| isPrivateSession | boolean | |
| isRestricted | boolean | Restricted devices cannot be controlled via the Web API. |
| volumePercent | number | Current volume 0–100, when the device reports it. |
PluginListenerHandle
| Prop | Type |
| ------------ | ----------------------------------------- |
| remove | () => Promise<void> |
ConnectionStateChange
| Prop | Type | Description |
| --------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| connected | boolean | |
| deviceId | string | Web only: the Web Playback SDK device ID (from the SDK ready event). |
| reason | 'error' | 'connect' | 'disconnect' | 'appBackgrounded' | |
| error | { code: SpotifyErrorCode; message: string; } | |
AuthStateChange
| Prop | Type | Description |
| ------------------- | -------------------- | ----------------------------------------------------- |
| authenticated | boolean | |
| expiresAt | number | Token expiry (epoch ms), present while authenticated. |
Type Aliases
RepeatMode
Repeat mode for the player.
off: no repeattrack: repeat the current trackcontext: repeat the current context (album, playlist, ...)
'off' | 'track' | 'context'
SpotifyErrorCode
Error codes attached to rejected calls.
Native rejections arrive as { message, code }; on web, thrown errors
carry the same code property. Switch on error.code in your app.
'NOT_INITIALIZED' | 'NOT_AUTHENTICATED' | 'AUTH_CANCELLED' | 'AUTH_FAILED' | 'TOKEN_REFRESH_FAILED' | 'SPOTIFY_APP_NOT_INSTALLED' | 'NOT_CONNECTED' | 'CONNECTION_FAILED' | 'PREMIUM_REQUIRED' | 'USER_NOT_AUTHORIZED' | 'UNSUPPORTED_VERSION' | 'OFFLINE' | 'NOT_ACTIVE_DEVICE' | 'NOT_SUPPORTED' | 'PLAYBACK_FAILED' | 'RATE_LIMITED' | 'UNKNOWN'
Error codes
| Code | Meaning |
| --- | --- |
| NOT_INITIALIZED | initialize() has not been called yet |
| NOT_AUTHENTICATED | No session — call authorize() |
| AUTH_CANCELLED | User dismissed the login/consent flow |
| AUTH_FAILED | Authorization failed (bad client ID, redirect mismatch, ...) |
| TOKEN_REFRESH_FAILED | Refresh grant failed; session was cleared |
| SPOTIFY_APP_NOT_INSTALLED | iOS/Android: Spotify app missing |
| NOT_CONNECTED | Player method called before connect() succeeded |
| CONNECTION_FAILED | Could not connect (on web often missing DRM/webview) |
| PREMIUM_REQUIRED | Operation needs a Premium account |
| USER_NOT_AUTHORIZED | User lacks app-remote-control scope or isn't on the app's user allowlist (development mode) |
| UNSUPPORTED_VERSION | Installed Spotify app is too old |
| OFFLINE | Network unavailable / Spotify app in offline mode |
| NOT_ACTIVE_DEVICE | Web: playback lives on another device |
| NOT_SUPPORTED | Not available on this platform (e.g. volume on iOS) |
| PLAYBACK_FAILED | A playback command failed |
| RATE_LIMITED | Web API 429 — retry later |
| UNKNOWN | Anything unmapped |
Troubleshooting
App Remote won't connect (iOS/Android) — the Spotify app must be installed, logged in, and have been launched at least once. On iOS it must be playing for a plain connect; use connect({ playUri: '' }) to let the plugin wake it (expect a brief app switch). On Android, connection errors surface as typed codes (SPOTIFY_APP_NOT_INSTALLED, OFFLINE, ...).
INVALID_CLIENT: Insecure redirect URI — your redirect URI isn't registered (exact match!), or the dashboard rejected a custom scheme. Try an all-lowercase, app-specific scheme with a host part (myapp://spotify-callback), or use App Links / Universal Links.
Web: CONNECTION_FAILED immediately — the environment has no EME/DRM (Capacitor webview, Chromium without Widevine, some privacy browsers). Check getCapabilities().webPlaybackViable. Ad blockers can also block sdk.scdn.co.
Web: authorize() seems to do nothing — it navigates the page to Spotify. Make sure you call initialize() on startup so the redirect back completes the flow, and that the page URL you started from is the registered redirect URI.
403 from playback calls — user isn't in your app's User Management allowlist (development mode, max 5), or isn't Premium.
PLAYBACK_FAILED: Cannot seek in song [CANT_PLAY_ON_DEMAND] — the account is playing in Free-tier (non-on-demand) mode; Spotify disallows seeking there. Check state.restrictions.canSeek and disable your seek UI when false — the other restrictions flags work the same way.
Android: setVolume fails with "No IAP endpoint" — many Spotify app builds don't expose local-device volume to App Remote. Treat volume control on Android as best-effort.
Android: FAIL_ON_PROJECT_REPOS build error — if your app opts into dependencyResolutionManagement { repositoriesMode = FAIL_ON_PROJECT_REPOS }, the plugin can't self-register its bundled Maven repo. Add it to your settings.gradle instead:
dependencyResolutionManagement {
repositories {
maven { url "$rootDir/../node_modules/@jacquesbeets/capacitor-spotify/android/repo" }
}
}Android build: "Cannot find a Java installation ... languageVersion=21" — Capacitor 8 builds with a Java 21 toolchain. Point JAVA_HOME at a JDK 21 (Android Studio's bundled JBR works: /Applications/Android Studio.app/Contents/jbr/Contents/Home).
Token expired after ~1 hour — expected; Spotify access tokens live 1 hour. The plugin refreshes automatically inside getAccessToken() and for the web player's getOAuthToken callback. If you cache the token string yourself, re-call getAccessToken() instead.
Bundled Spotify SDK versions
| SDK | Version | How it ships |
| --- | --- | --- |
| Spotify iOS SDK (SpotifyiOS.xcframework) | 5.0.1 | Vendored in ios/ (CocoaPods) + official SPM package pin |
| Spotify Android App Remote | 0.8.0 | Vendored AAR in android/repo/ (not on Maven Central) |
| Spotify Android auth library | 5.0.0 | Maven Central (com.spotify.android:auth) |
| Web Playback SDK | rolling | Loaded at runtime from sdk.scdn.co |
Upgrading the iOS SDK: bump the exact: pin in Package.swift and replace ios/SpotifyiOS.xcframework from the same tag in one commit.
Future work
Web API player helpers (queue, device list, transfer), ContentApi browsing, library add/remove + user capabilities (UserApi), user profile, token-swap server recipe, EncryptedSharedPreferences, CI.
License
This plugin is MIT licensed.
The vendored Spotify SDK binaries (ios/SpotifyiOS.xcframework, android/repo/.../app-remote-0.8.0.aar) remain subject to Spotify's own terms — see SPOTIFY_SDK_LICENSES/ and the Spotify Developer Terms. Note that Spotify's SDK/Web Playback terms restrict commercial use without prior written approval from Spotify — review them for your use case.
