February 2026 Web API Dev Mode Changes - Migration Guide
This guide is here to help developers with existing apps migrate to the updated Web API following the February 2026 announcement. It covers what's changing, who is affected, and provides step-by-step migration paths for common use cases.
Who is affected?
Extended Quota Mode apps: No migration required. Apps in extended quota mode are not affected by any of the changes described in this guide — all existing endpoints, fields, and behaviors remain unchanged. You can optionally adopt the new generic library endpoints (PUT/DELETE /me/library) but your existing integrations will remain fully functional.
Development Mode apps: This guide is for you. Read on for the full details of what's changing and how to update your app.
Timeline
| Date | What happens |
|---|---|
| February 11, 2026 | New Development Mode apps are created with new restrictions |
| March 9, 2026 | Existing Development Mode apps are migrated to new restrictions |
Account and App Limit Changes
Premium Requirement
All Development Mode apps require the app owner to have an active Spotify Premium subscription. If the owner's Premium subscription lapses, the app will stop working. It will resume functioning once the owner resubscribes.
App Limits
| Requirement | New apps |
|---|---|
| Client IDs per developer | 1 |
| Users per app | 5 |
Existing apps are grandfathered: If you already have multiple Client IDs or more than 5 users, you will retain them. These limits only restrict what you can create or add going forward.
Endpoint Changes
Library Management
The entity type specific save, remove, follow, and unfollow endpoints have been replaced with generic library endpoints that work with Spotify URIs.
Saving items and following
Before:
_10PUT /me/tracks_10PUT /me/albums_10PUT /me/episodes_10PUT /me/shows_10PUT /me/audiobooks_10PUT /me/following_10PUT /playlists/{id}/followers
After:
_10PUT /me/library
The new PUT /me/library endpoint accepts Spotify URIs instead of IDs, allowing you to save or follow any supported content type in a single request. Appropriate scopes need to be passed depending on the entities being saved.
Removing items and unfollowing
Before:
_10DELETE /me/tracks_10DELETE /me/albums_10DELETE /me/episodes_10DELETE /me/shows_10DELETE /me/audiobooks_10DELETE /me/following_10DELETE /playlists/{id}/followers
After:
_10DELETE /me/library
Like the save endpoint, DELETE /me/library accepts Spotify URIs instead of IDs.
Checking if items are saved
The individual "contains" endpoints have been replaced with a single generic endpoint.
Before:
_10GET /me/tracks/contains_10GET /me/albums/contains_10GET /me/episodes/contains_10GET /me/shows/contains_10GET /me/audiobooks/contains_10GET /me/following/contains_10GET /playlists/{id}/followers/contains
After:
_10GET /me/library/contains
The new GET /me/library/contains endpoint accepts Spotify URIs instead of IDs.
Playlist Endpoint Renames
The playlist track management endpoints have been renamed from /tracks to /items:
| Removed endpoint | Replacement | Comment |
|---|---|---|
POST /playlists/{id}/tracks | POST /playlists/{id}/items | |
GET /playlists/{id}/tracks | GET /playlists/{id}/items | Only available for playlists the user owns or collaborates on. Fields have been renamed in the response, see Playlist field changes for response changes. |
DELETE /playlists/{id}/tracks | DELETE /playlists/{id}/items | Parameter tracks renamed to items |
PUT /playlists/{playlist_id}/tracks | PUT /playlists/{id}/items |
Batch/Bulk Fetch Endpoints (Removed)
The following batch endpoints are no longer available. Fetch items individually instead.
| Removed endpoint | Replacement |
|---|---|
GET /tracks | GET /tracks/{id} (one request per track) |
GET /albums | GET /albums/{id} |
GET /artists | GET /artists/{id} |
GET /episodes | GET /episodes/{id} |
GET /shows | GET /shows/{id} |
GET /audiobooks | GET /audiobooks/{id} |
GET /chapters | GET /chapters/{id} |
Migration example:
_11// Before: Batch fetch_11const response = await fetch('/v1/tracks?ids=id1,id2,id3');_11const { tracks } = await response.json();_11_11// After: Individual fetches_11const trackIds = ['id1', 'id2', 'id3'];_11const tracks = await Promise.all(_11 trackIds.map(id =>_11 fetch(`/v1/tracks/${id}`).then(r => r.json())_11 )_11);
Browse and Artist Endpoints (Removed)
| Removed endpoint | Description |
|---|---|
GET /browse/new-releases | New album releases |
GET /browse/categories | Browse categories list |
GET /browse/categories/{id} | Single category details |
GET /artists/{id}/top-tracks | Artist's top tracks by country |
Other User Data (Removed)
| Removed endpoint | Replacement |
|---|---|
GET /users/{id} | No replacement - use GET /me for current user |
GET /users/{id}/playlists | No replacement - use GET /me/playlists for current user |
POST /users/{user_id}/playlists | POST /me/playlists |
GET /markets | No replacement |
Field Changes by Content Type
The following field changes apply across all endpoints that return these content types.
Removed fields
The following fields have been removed from response objects. Update your code to handle their absence gracefully.
Track
available_markets, external_ids, linked_from, popularity
Album
album_group, available_markets, external_ids, label, popularity
Artist
followers, popularity
User (from GET /me)
country, email, explicit_content, followers, product
Show
available_markets, publisher
Audiobook / Chapter
available_markets, publisher (audiobook only)
Renamed fields
Playlist
The tracks field has been renamed:
| Before | After |
|---|---|
tracks | items |
tracks.tracks | items.items |
tracks.tracks.track | items.items.item |
Important: Playlist contents (items) are only returned for playlists the user owns or collaborates on. For other playlists, only metadata is returned and the items field will be absent from the response.
Migration Checklist
Use this checklist to ensure your app is ready:
- Account: Ensure the app owner has Spotify Premium
- Library endpoints: Replace content-specific save/remove/follow/unfollow calls with
PUT/DELETE /me/library - Contains checks: Replace content-specific "contains" calls with
GET /me/library/contains - Batch fetches: Replace batch endpoints with individual fetch calls
- Browse and artist endpoints: Remove features using browse categories, new releases, or artist top tracks
- Other users: Remove features that fetch other users' profiles or playlists
- Playlist endpoints: Update
/playlists/{id}/trackscalls to/playlists/{id}/items - Playlist handling: Update code for the
tracks→itemsfield rename - Removed fields: Handle missing fields gracefully (check for undefined/null)
- Global recommendation: Limit API calls to avoid hitting rate limits
Common Migration Patterns
Pattern 1: Updating library save calls
_14// Before_14await spotify.put('/me/tracks', { ids: ['trackId1', 'trackId2'] });_14await spotify.put('/me/albums', { ids: ['albumId1'] });_14await spotify.put('/me/following', { ids: ['artistId1'], type: 'artist' });_14_14// After — all entity types in a single call_14await spotify.put('/me/library', {_14 uris: [_14 'spotify:track:trackId1',_14 'spotify:track:trackId2',_14 'spotify:album:albumId1',_14 'spotify:artist:artistId1'_14 ]_14});
Pattern 2: Handling missing popularity field
_10// Before_10const sortedTracks = tracks.sort((a, b) => b.popularity - a.popularity);_10_10// After - popularity is no longer available_10// Consider alternative sorting criteria or remove this feature_10const sortedTracks = tracks.sort((a, b) =>_10 a.name.localeCompare(b.name)_10);
Pattern 3: Handling playlist field rename
_12// Before_12const trackCount = playlist.tracks.total;_12const firstTrack = playlist.tracks.items[0].track;_12_12// After_12const trackCount = playlist.items?.total ?? 0;_12const firstTrack = playlist.items?.items?.[0]?.item;_12_12// Note: items may be undefined for playlists you don't own_12if (!playlist.items) {_12 console.log('Track details not available for this playlist');_12}