Connect Spotify
Add Spotify to Minion as a custom PKCE OAuth app, connect it, and call the Web API from execute or a package without handling tokens.
Connecting Spotify lets your agents and packages read your listening history, current playback and library through the Spotify Web API at api.spotify.com, without the token ever reaching their code.
Minion has no built-in Spotify preset. You add Spotify as a custom OAuth app. Spotify supports PKCE, so you don't give Minion a client secret at all.
Before you start
- You need to be an owner or admin of your Minion organization. Members can use an integration but can't add an app or connect one.
- You need a Spotify account that can create apps in the Spotify developer dashboard .
- Spotify's Development Mode has limits. Check the current rules on Spotify's site before you rely on them:
- Since February 2026, Development Mode requires the app owner to have Spotify Premium. If Premium lapses, the app stops working until it's restored.
- Some Web API endpoints aren't available to Development Mode apps. See Spotify's February 2026 migration guide .
- Only the app owner and up to 5 users you add under User Management can authorize a Development Mode app.
- Refresh tokens expire six months after you first authorize. Refreshing doesn't extend that. Plan to reconnect about twice a year.
Create the app on Spotify
- Open the Spotify developer dashboard and click Create app.
- Give it a name and a description.
- Add this redirect URI, exactly:
https://app.minion.codes/oauth/callback - Under the APIs used, select Web API. Accept the terms and save.
- Copy the Client ID from the app page. You don't need the client secret.
- If someone other than you will authorize the app, add their Spotify account under User Management. For personal use, skip this.
Add it in Minion
- Open app.minion.codes/app/integrations .
- In the OAuth apps card, set Provider to Custom (any OAuth 2 provider).
- Set App name (optional) to
spotify. Don't leave it empty: a custom app's name defaults tocustom. - Paste the Client ID. Leave Client secret empty.
- Set Default scopes (optional) to the scopes you want, separated by spaces. For example:
user-read-recently-played user-read-playback-state user-read-currently-playing user-library-read. - Set API hosts (custom: required) to
api.spotify.com. - Open Custom provider endpoints and fill in:
- Authorization URL:
https://accounts.spotify.com/authorize - Token URL:
https://accounts.spotify.com/api/token - Revocation URL (optional): leave empty. Spotify has no revocation endpoint.
- Client authentication: None (public client, PKCE)
- Keep Use PKCE (S256) checked.
- Authorization URL:
- Click Save app. The app appears in the list as
custom · PKCE · public client.
Pick scopes for what you'll actually do:
user-read-recently-played: listening history.user-read-playback-stateanduser-read-currently-playing: what's playing and on which device.user-library-read: saved tracks and albums.playlist-read-private: your private playlists.user-top-read: top artists and tracks.user-modify-playback-state: play, pause, skip and queue. This controls your real devices, so only add it when you want that.
Connect
- In the Integrations card, choose
spotifyunder OAuth app. - Leave Name (optional) empty to call the integration
spotify. - Leave Extra scopes (optional) empty to use the app's default scopes. On a first connect, anything you type here replaces the defaults, so list every scope you need.
- Click Connect and approve on Spotify.
Spotify sends you back to the Integrations page with Connected "spotify". and status connected. The account column stays empty for custom apps. That is expected.
An agent can start the same flow with minion.integrations.connect({ provider: 'spotify' }). It returns a connectUrl like https://app.minion.codes/app/integrations/connect?name=spotify. An owner or admin opens it while signed in to Minion and approves within 10 minutes.
To add a scope later, type it in Extra scopes (optional) and click Connect again, or call minion.integrations.connect({ provider: 'spotify', scopes: ['user-top-read'] }). Once the integration exists, new scopes add to the ones you asked for before. To drop a scope, click Disconnect and connect again.
Check it works
Run this with execute. It only reads your Spotify profile.
export default async function main() {
const integration = await minion.integrations.get({ name: 'spotify' })
if (integration.status !== 'connected') {
return { status: integration.status, reconnectUrl: integration.reconnectUrl }
}
const res = await minion.integrations.fetch({ name: 'spotify', url: 'https://api.spotify.com/v1/me' })
if (!res.ok) throw new Error(`Spotify returned ${res.status}: ${await res.text()}`)
const me = await res.json()
return { id: me.id, displayName: me.display_name, scopes: integration.scopes }
} Your Spotify user id in the result means the integration, its token and the allowed host all line up.
Use it in a package
In package code, import createAuthenticatedFetch from minion:runtime. It returns a fetch function bound to the integration. Minion adds the Authorization header, refreshes the token when it expires or when Spotify answers 401, and only sends it to api.spotify.com.
package.json:
{
"name": "@your-org/spotify-tools",
"exports": { "./recent": "./src/recent.ts" }
} src/recent.ts:
import { createAuthenticatedFetch } from 'minion:runtime'
const spotify = createAuthenticatedFetch('spotify')
export default async function main() {
const res = await spotify('https://api.spotify.com/v1/me/player/recently-played?limit=20')
if (!res.ok) throw new Error(`Spotify returned ${res.status}`)
const { items } = await res.json()
return items.map((i) => ({ track: i.track.name, artist: i.track.artists[0]?.name, playedAt: i.played_at }))
} Save and publish it with minion.packages.save and publish: true. Publishing needs an owner or admin and a README.md that says the package needs the spotify integration. Other code can only import a published version. Then call it from execute:
import recent from 'minion:@your-org/spotify-tools/recent'
export default async function main() {
return recent()
} Replace your-org with your organization slug. See /docs/packages for saving and publishing.
Troubleshooting
- INVALID_CLIENT: Invalid redirect URI. The redirect URI on the Spotify app must be exactly
https://app.minion.codes/oauth/callback. No trailing slash. - "This app needs a client secret (or token auth "none" for a public PKCE client)." You left the secret empty but kept Client authentication on Secret in the request body. Pick None (public client, PKCE).
- "List at least one API host the token may be sent to". Fill in API hosts with
api.spotify.com. - An error saying integration "spotify" may not send its token to a host. Your code called a host outside the app's API hosts. The Web API lives on
api.spotify.com. To change the list, submit the OAuth apps form again with the same app name and the client ID, endpoints and hosts filled in. A blank client secret keeps the current one. - "This connect link expired, was already used, or was started by someone else." Start again from the Integrations page. A link lasts 10 minutes, and the person who opened it has to finish it.
- 403 User not registered in the Developer Dashboard. Someone other than the app owner tried to authorize. Add them under User Management on Spotify.
- 403 on one endpoint while others work. That endpoint may not be available in Development Mode. Check Spotify's migration guide.
- Everything fails at once. Check that the app owner's Spotify Premium is active.
- The integration turns error and lastError mentions invalid_grant. The refresh token reached its six-month limit, or you removed the app's access in Spotify. Minion fires an
integration.auth.failedevent with areconnectUrl. Click Connect next to the integration on the Integrations page, or open thereconnectUrlfromminion.integrations.get({ name: 'spotify' }). A package can subscribe tointegration.auth.failedto alert you.
Disconnecting the integration in Minion deletes the token in Minion but doesn't revoke it at Spotify. Remove the app's access in your Spotify account settings too.
For how OAuth integrations work in general, see /docs/oauth . To run code, see /docs/execute .