Automate Spotify playlist curation by searching tracks via GET /v1/search?type=track, creating playlists via POST /v1/users/{user_id}/playlists, and adding tracks via POST /v1/playlists/{id}/tracks. Requires OAuth 2.0 Authorization Code flow with playlist-modify-public or playlist-modify-private scopes. Rate limits use a rolling 30-second window with undisclosed thresholds — batch track adds in groups of 100 with 1-second delays to stay safe.
API Quick Reference
OAuth 2.0
Rolling 30-second window (exact threshold undisclosed)
JSON
Available
Understanding the Spotify Web API
The Spotify Web API is a REST API that gives you access to the Spotify music catalog, user library, playlists, and playback controls. All requests go to https://api.spotify.com/v1 and return JSON. The API uses OAuth 2.0 for authentication — catalog-only automations can use Client Credentials flow, but anything involving a user's playlists requires the Authorization Code flow (or PKCE for browser-based apps).
Playlist curation is one of the most accessible Spotify automations because search and playlist management endpoints remain fully available to all new apps. Unlike recommendations or audio features — which were deprecated for new apps on November 27, 2024 — the search and playlist endpoints work without any special quota approval.
Before building: be aware that Development Mode limits you to 5 authorized Spotify users per Client ID, and the app owner must hold a Spotify Premium subscription. For production apps serving more users, you need Extended Quota Mode, which as of May 2025 requires a legally registered organization with 250,000+ monthly active users. Official docs: https://developer.spotify.com/documentation/web-api
https://api.spotify.com/v1Setting Up Spotify API Authentication
Spotify uses OAuth 2.0 Authorization Code flow for user-specific operations like playlist creation. You exchange a code for an access token (valid 1 hour) and a refresh token (long-lived). Client Credentials flow only works for public catalog data — it cannot create or modify playlists because those operations require user identity.
- 1Go to https://developer.spotify.com/dashboard and log in with your Spotify Premium account
- 2Click 'Create app', fill in the app name, description, and set a redirect URI (e.g. http://localhost:8888/callback)
- 3Note your Client ID and Client Secret from the app settings page
- 4Request the authorization URL: GET https://accounts.spotify.com/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REDIRECT_URI&scope=playlist-modify-public%20playlist-modify-private
- 5The user is redirected to YOUR_REDIRECT_URI?code=AUTH_CODE — extract the code parameter
- 6Exchange the code for tokens via POST https://accounts.spotify.com/api/token with grant_type=authorization_code, code, redirect_uri, and Authorization: Basic base64(client_id:client_secret)
- 7Store the access_token (expires in 3600s) and refresh_token securely in environment variables
- 8When the access token expires, POST to https://accounts.spotify.com/api/token with grant_type=refresh_token and your refresh_token to get a new one
1import os2import base643import requests45CLIENT_ID = os.environ['SPOTIFY_CLIENT_ID']6CLIENT_SECRET = os.environ['SPOTIFY_CLIENT_SECRET']7REFRESH_TOKEN = os.environ['SPOTIFY_REFRESH_TOKEN']89def get_access_token():10 """Exchange refresh token for a new access token."""11 credentials = base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()12 response = requests.post(13 'https://accounts.spotify.com/api/token',14 headers={'Authorization': f'Basic {credentials}'},15 data={16 'grant_type': 'refresh_token',17 'refresh_token': REFRESH_TOKEN18 }19 )20 response.raise_for_status()21 return response.json()['access_token']2223token = get_access_token()24headers = {'Authorization': f'Bearer {token}'}Security notes
- •Never hardcode Client ID, Client Secret, or tokens — store them in environment variables
- •Never expose Client Secret in frontend or client-side code — it belongs server-side only
- •Access tokens expire after 3,600 seconds (1 hour) — always check expiry and refresh proactively
- •Scope-limit your app: only request playlist-modify-public if you only need public playlists
- •Rotate Client Secrets periodically from the Spotify Developer Dashboard
- •If your refresh token is compromised, revoke it immediately from the dashboard and re-authorize
Key endpoints
/v1/searchSearch the Spotify catalog for tracks, artists, albums, or playlists. Use type=track for playlist curation and filter results by genre, year, or popularity.
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | required | Search query — supports field filters like 'genre:rock year:2023' |
type | string | required | Comma-separated list of item types: track, artist, album, playlist |
limit | number | optional | Number of results per page, 1-50, default 20 |
offset | number | optional | Offset for pagination, max 1000 |
market | string | optional | ISO 3166-1 alpha-2 country code to filter available tracks |
Response
1{"tracks":{"items":[{"id":"4iV5W9uYEdYUVa79Axb7Rh","name":"Bohemian Rhapsody","artists":[{"name":"Queen"}],"album":{"name":"A Night at the Opera","release_date":"1975-11-21"},"popularity":82,"uri":"spotify:track:4iV5W9uYEdYUVa79Axb7Rh"}],"total":847,"next":"https://api.spotify.com/v1/search?q=bohemian+rhapsody&type=track&offset=20"}}/v1/users/{user_id}/playlistsCreate a new empty playlist for a user. Requires playlist-modify-public or playlist-modify-private scope depending on the public field.
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string | required | The Spotify user ID of the playlist owner — get it from GET /v1/me |
name | string | required | Name for the new playlist |
public | boolean | optional | Whether the playlist is public, default true |
description | string | optional | Playlist description shown in Spotify clients |
Request
1{"name":"My Curated Playlist","description":"Auto-generated from rock hits 2023","public":false}Response
1{"id":"3cEYpjA9oz9GiPac4AsH4n","name":"My Curated Playlist","external_urls":{"spotify":"https://open.spotify.com/playlist/3cEYpjA9oz9GiPac4AsH4n"},"snapshot_id":"MTY2LDljYzE0OTM4MjVkN2UxMjdmNzJiNGI4YjBiZmMxMzIxMzExYjM0ZQ==","tracks":{"total":0}}/v1/playlists/{playlist_id}/tracksAdd one or more tracks to a playlist. Accepts up to 100 track URIs per request. Returns a snapshot_id you can use to verify the add succeeded.
| Parameter | Type | Required | Description |
|---|---|---|---|
playlist_id | string | required | Spotify playlist ID from the create response |
uris | array | required | Array of Spotify track URIs, max 100 per request |
position | number | optional | Zero-based position to insert tracks, default appends to end |
Request
1{"uris":["spotify:track:4iV5W9uYEdYUVa79Axb7Rh","spotify:track:0VjIjW4GlUZAMYd2vXMi3b"],"position":0}Response
1{"snapshot_id":"MTY2LDljYzE0OTM4MjVkN2UxMjdmNzJiNGI4YjBiZmMxMzIxMzExYjM0ZQ=="}/v1/me/playlistsList the current user's playlists to check for an existing playlist by name before creating a duplicate.
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | number | optional | Number of playlists to return, 1-50, default 20 |
offset | number | optional | Offset for pagination |
Response
1{"items":[{"id":"3cEYpjA9oz9GiPac4AsH4n","name":"My Curated Playlist","tracks":{"total":12},"snapshot_id":"MTY2..."}],"total":8,"next":null}Step-by-step automation
Get Your Spotify User ID
Why: Playlist creation requires the user's Spotify ID, not just an access token.
Call GET /v1/me with your access token to retrieve the authenticated user's profile including their id field. Store this — you'll pass it as user_id when creating playlists. This also confirms your token has the correct scopes and hasn't expired.
1curl -X GET 'https://api.spotify.com/v1/me' \2 -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'Pro tip: Cache the user ID — it never changes. Avoid calling /me on every run.
Expected result: JSON response with display_name, id, email (if user-read-email scope), and country fields.
Search for Tracks by Genre or Keyword
Why: The search endpoint is the primary way to discover tracks for curation since /recommendations is deprecated for new apps.
Send GET /v1/search?q=genre:rock year:2024&type=track&limit=50 to get matching tracks. Spotify's query syntax supports field filters: genre, year, artist, album, track, isrc, upc. Paginate using offset if you need more than 50 results. Extract the track URIs (spotify:track:ID) from each result — these are what you add to playlists.
1curl -G 'https://api.spotify.com/v1/search' \2 -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \3 --data-urlencode 'q=genre:indie-rock year:2024' \4 --data-urlencode 'type=track' \5 -d 'limit=50' \6 -d 'market=US'Pro tip: Use 'year:2024' or 'year:2020-2024' in your query to get recent releases. Sort by popularity descending to get the most relevant tracks at the top.
Expected result: Array of track objects each containing id, name, artists, popularity, and uri. Popularity is 0-100 — higher values mean more popular recently.
Create or Retrieve the Target Playlist
Why: Before adding tracks, you need a playlist ID — either by creating a new one or finding an existing one to update.
Call POST /v1/users/{user_id}/playlists to create a new playlist. If you want to update an existing weekly playlist instead of creating a new one each run, first call GET /v1/me/playlists to find it by name. Store the returned playlist id — you'll need it for the next step.
1curl -X POST 'https://api.spotify.com/v1/users/YOUR_USER_ID/playlists' \2 -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \3 -H 'Content-Type: application/json' \4 -d '{"name":"Weekly Indie Rock Picks","description":"Auto-curated from indie-rock genre, updated weekly","public":false}'Pro tip: For a 'living' playlist that refreshes weekly, use PUT /v1/playlists/{id}/tracks (not POST) to replace all tracks at once instead of removing then adding.
Expected result: JSON with the new playlist's id, name, snapshot_id, and external_urls.spotify (the share link).
Add Tracks to the Playlist in Batches
Why: The API accepts up to 100 track URIs per add request — batching is required for larger track lists and prevents hitting rate limits.
POST your track URIs to /v1/playlists/{playlist_id}/tracks in batches of 100 (the API maximum). Add a 1-second delay between batches to stay well under the rolling 30-second rate limit window. The API returns a snapshot_id on each successful add — log it to verify the playlist state. Due to eventual consistency, tracks may take a few seconds to appear in the Spotify client after a successful API response.
1curl -X POST 'https://api.spotify.com/v1/playlists/PLAYLIST_ID/tracks' \2 -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \3 -H 'Content-Type: application/json' \4 -d '{"uris":["spotify:track:4iV5W9uYEdYUVa79Axb7Rh","spotify:track:0VjIjW4GlUZAMYd2vXMi3b"]}'Pro tip: To fully replace a playlist's tracks (e.g., for a weekly refresh), use PUT /v1/playlists/{id}/tracks instead of POST. PUT replaces all existing tracks and accepts up to 100 URIs in a single request.
Expected result: Each batch returns {"snapshot_id": "..."}. The snapshot_id changes every time the playlist is modified — use it to confirm the add succeeded.
Complete working code
This script runs a complete playlist curation cycle: it gets the user's Spotify ID, searches the catalog for tracks matching a configurable genre and year, creates (or updates) a private playlist, and adds the top tracks in batches. Token refresh is handled automatically. Set SPOTIFY_PLAYLIST_NAME to an existing playlist name to update it instead of creating a new one each run.
1import os2import base643import time4import requests56CLIENT_ID = os.environ['SPOTIFY_CLIENT_ID']7CLIENT_SECRET = os.environ['SPOTIFY_CLIENT_SECRET']8REFRESH_TOKEN = os.environ['SPOTIFY_REFRESH_TOKEN']9GENRE = os.environ.get('SPOTIFY_GENRE', 'indie-rock')10YEAR = os.environ.get('SPOTIFY_YEAR', '2024')11PLAYLIST_NAME = os.environ.get('SPOTIFY_PLAYLIST_NAME', f'Weekly {GENRE.title()} Picks')12TRACK_COUNT = int(os.environ.get('SPOTIFY_TRACK_COUNT', '20'))1314def get_access_token():15 creds = base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()16 r = requests.post(17 'https://accounts.spotify.com/api/token',18 headers={'Authorization': f'Basic {creds}'},19 data={'grant_type': 'refresh_token', 'refresh_token': REFRESH_TOKEN}20 )21 r.raise_for_status()22 return r.json()['access_token']2324def spotify_get(token, path, params=None):25 r = requests.get(f'https://api.spotify.com/v1{path}',26 headers={'Authorization': f'Bearer {token}'}, params=params)27 if r.status_code == 429:28 wait = int(r.headers.get('Retry-After', 5))29 print(f'Rate limited, waiting {wait}s...')30 time.sleep(wait)31 r = requests.get(f'https://api.spotify.com/v1{path}',32 headers={'Authorization': f'Bearer {token}'}, params=params)33 r.raise_for_status()34 return r.json()3536def spotify_post(token, path, payload):37 r = requests.post(f'https://api.spotify.com/v1{path}',38 headers={'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'},39 json=payload)40 r.raise_for_status()41 return r.json()4243def main():44 token = get_access_token()45 user = spotify_get(token, '/me')46 user_id = user['id']47 print(f'Authenticated as {user_id}')4849 # Search for tracks50 search_data = spotify_get(token, '/search', {51 'q': f'genre:{GENRE} year:{YEAR}',52 'type': 'track', 'limit': 50, 'market': 'US'53 })54 tracks = sorted(search_data['tracks']['items'],55 key=lambda t: t['popularity'], reverse=True)56 track_uris = [t['uri'] for t in tracks[:TRACK_COUNT]]57 print(f'Found {len(track_uris)} tracks to add')5859 # Find or create playlist60 playlists = spotify_get(token, '/me/playlists', {'limit': 50})61 existing = next((p for p in playlists['items'] if p['name'] == PLAYLIST_NAME), None)6263 if existing:64 playlist_id = existing['id']65 # Replace all tracks66 requests.put(f'https://api.spotify.com/v1/playlists/{playlist_id}/tracks',67 headers={'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'},68 json={'uris': track_uris[:100]}).raise_for_status()69 print(f'Updated playlist: {playlist_id}')70 else:71 playlist = spotify_post(token, f'/users/{user_id}/playlists', {72 'name': PLAYLIST_NAME, 'public': False,73 'description': f'Auto-curated {GENRE} tracks from {YEAR}'74 })75 playlist_id = playlist['id']76 spotify_post(token, f'/playlists/{playlist_id}/tracks', {'uris': track_uris})77 print(f'Created playlist: {playlist["external_urls"]["spotify"]}')7879if __name__ == '__main__':80 main()Error handling
{"error":{"status":401,"message":"No token provided"}}Access token is missing, malformed, or expired. Tokens last exactly 3,600 seconds.
Refresh your access token using the refresh token and grant_type=refresh_token. Never hardcode tokens — always load from environment and handle expiry proactively.
Refresh token immediately, then retry the original request once. Do not loop more than twice.
{"error":{"status":403,"message":"Insufficient client scope"}}Your access token was issued without the required scope. Playlist creation needs playlist-modify-public or playlist-modify-private. Also returned for deprecated endpoints (recommendations, audio-features, etc.) on new apps.
Re-authorize the user requesting the missing scope in your authorization URL. For deprecated endpoints, there is no fix — those features are permanently unavailable for apps created after November 27, 2024.
Not retryable — fix the scope or drop the endpoint.
{"error":{"status":404,"message":"Resource not found"}}Playlist or track ID is invalid or the resource was deleted. Also returned by some deprecated endpoints.
Verify the playlist ID or track URI. Re-fetch the list of playlists to confirm the target still exists before adding tracks.
Not retryable — validate IDs before each operation.
{"error":{"status":429,"message":"API rate limit exceeded"}}Too many requests in the rolling 30-second window. Development Mode has a lower ceiling than Extended Quota Mode.
Read the Retry-After header and sleep exactly that many seconds before retrying. Add 1-second delays between batch operations proactively to avoid hitting the limit in the first place.
Honor Retry-After header exactly. Implement exponential backoff as a secondary fallback: 1s, 2s, 4s, 8s.
Rate Limits for Spotify API
| Scope | Limit | Window |
|---|---|---|
| Per app (Development Mode) | Undisclosed threshold | Rolling 30 seconds (lower ceiling than Extended Quota) |
| Per app (Extended Quota Mode) | Undisclosed threshold | Rolling 30 seconds (higher ceiling, requires 250k+ MAU org) |
| Track adds per request | 100 track URIs | Per API call |
1import time23def spotify_request_with_retry(func, max_retries=5):4 for attempt in range(max_retries):5 try:6 return func()7 except requests.exceptions.HTTPError as e:8 if e.response.status_code == 429:9 retry_after = int(e.response.headers.get('Retry-After', 2 ** attempt))10 print(f'Rate limited. Waiting {retry_after}s (attempt {attempt+1}/{max_retries})')11 time.sleep(retry_after)12 else:13 raise14 raise Exception('Max retries exceeded')- Add 1-second delays between batch operations even when not rate limited — prevents bursts
- Always read and honor the Retry-After header on 429 responses — do not use fixed sleep values
- Cache user IDs, playlist IDs, and token values to minimize unnecessary API calls per run
- In Development Mode, test with a single user and small batches before scaling up
- Log request timestamps to identify patterns if you start seeing unexpected 429s
Security checklist
- Store Client ID, Client Secret, and refresh tokens in environment variables — never in source code
- Never expose Client Secret in client-side JavaScript or mobile app bundles
- Use HTTPS for all API calls and your redirect URI — Spotify rejects plain HTTP redirect URIs in production
- Scope-limit your OAuth request — only request playlist-modify-public if you only need public playlists
- Rotate Client Secrets from developer.spotify.com/dashboard if you suspect exposure
- Store refresh tokens encrypted at rest if persisting them in a database
- In Development Mode, only authorize the 5 user slots you actually need — do not share test credentials
- Validate redirect URIs match exactly what you registered — never use wildcards in production apps
Automation use cases
Weekly Genre Digest
beginnerRun every Monday morning, search for the week's top tracks in a specific genre, and refresh a shared playlist with the results.
Artist-Based Playlist Builder
beginnerGiven a list of seed artists, search for their latest tracks and compile a chronological discography playlist.
Multi-Source Curation Engine
intermediateCombine tracks from search results, the user's saved library, and followed artists' latest releases into a personalized weekly playlist.
Mood-Based Playlist Rotator
intermediateMaintain multiple named playlists (Morning Focus, Evening Wind-Down) and refresh each with keyword-matched tracks on a schedule.
No-code alternatives
Don't want to write code? These platforms can automate the same workflows visually.
Zapier
Free tier available; paid plans from $19.99/monthZapier's Spotify integration supports adding tracks to playlists triggered by events in other apps (new RSS item, new spreadsheet row, etc.).
- + No code required
- + Easy to connect with 7,000+ other apps
- + Runs on Zapier's infrastructure
- - Limited to Zapier's pre-built Spotify actions — no custom search logic
- - Free tier limited to 100 tasks/month
- - Less flexible than direct API for complex curation logic
Make
Free tier available; paid plans from $9/monthMake's Spotify module supports searching tracks and managing playlists with more flexible routing logic than Zapier.
- + Visual workflow builder
- + More granular HTTP modules for custom API calls
- + Free tier includes 1,000 operations/month
- - Learning curve for complex routing scenarios
- - Spotify module covers only common endpoints
- - Paid plans required for high-volume curation
n8n
Self-hosted free; cloud from €20/monthn8n's Spotify node covers search and playlist management; self-hosted option gives you unlimited runs with no per-operation cost.
- + Self-hosted = free unlimited executions
- + Full HTTP Request node for any endpoint the GUI doesn't expose
- + Open source with active community
- - Requires server setup for self-hosting
- - Cloud version has per-execution pricing
- - Less polished than Zapier for quick setups
Best practices
- Always dedup track URIs before adding to a playlist — Spotify allows duplicate tracks and will add them without error
- Use PUT /v1/playlists/{id}/tracks (replace) instead of POST (append) for weekly refreshes to avoid accumulating hundreds of tracks
- Sort search results by popularity descending before selecting tracks — Spotify's search ranking isn't always quality-ordered
- Check response snapshot_id after each add to confirm the operation succeeded before moving to the next batch
- Handle eventual consistency: tracks added via API may take a few seconds to appear on Spotify clients — don't interpret this as an error
- Test in Development Mode with your own account before requesting Extended Quota — the 5-user limit is sufficient for personal automation
- Log the playlist URL from external_urls.spotify after creation — useful for quick manual verification runs
Ask AI to help
Copy one of these prompts to get a personalized, working implementation.
I'm building a Spotify playlist curation automation using the Spotify Web API. My app uses OAuth 2.0 Authorization Code flow with playlist-modify-private scope. I search for tracks via GET /v1/search and add them via POST /v1/playlists/{id}/tracks. I'm getting a 403 error after the token is successfully retrieved. The search endpoint works fine but playlist creation fails. My access token was obtained with these scopes: [paste your scope string]. What could cause a 403 specifically on playlist creation but not search?
Build a Spotify playlist curation dashboard with these features: 1) OAuth login button that initiates the Spotify Authorization Code flow, 2) A genre/keyword search input that calls GET /v1/search and displays results in a card grid with track name, artist, album art, and popularity score, 3) A 'Create Playlist' form with name and description fields, 4) A track selector that lets users pick tracks from search results and add them to the new playlist via POST /v1/playlists/{id}/tracks. Show loading states, handle 401 token refresh, and display the final playlist URL on success. Use Tailwind CSS for styling.
Frequently asked questions
Is the Spotify API free?
Yes — the Spotify Web API is free to use. In Development Mode, you can make API calls at no cost but are limited to 5 authorized users per Client ID (as of February 11, 2026), and the app owner must have Spotify Premium. For production apps serving more than 5 users, Extended Quota Mode is required, which since May 2025 is only available to legally registered organizations with 250,000+ monthly active users.
Can I use the /recommendations endpoint to curate playlists?
No — GET /recommendations was permanently deprecated for apps created after November 27, 2024 and returns HTTP 403. The working alternative is GET /v1/search with genre and year filters to discover tracks, combined with GET /v1/me/top/tracks to understand the user's taste. See our song recommendations page for a detailed migration guide.
What happens when I hit the rate limit?
You receive HTTP 429 with a Retry-After header indicating how many seconds to wait. The exact threshold is undisclosed by Spotify and uses a rolling 30-second window. Always read Retry-After and sleep exactly that duration before retrying. Proactively add 1-second delays between batch operations to avoid hitting the limit in the first place.
Why does my playlist creation return 403 even though my token is valid?
The most common cause is missing OAuth scopes. Playlist creation requires playlist-modify-public for public playlists or playlist-modify-private for private ones — Client Credentials flow cannot create playlists at all since it has no user identity. Re-authorize the user with the correct scope in your authorization URL. If the scope is correct, check that you're using the Authorization Code flow token, not a Client Credentials token.
How many tracks can I add to a playlist per request?
The API accepts a maximum of 100 track URIs per POST /v1/playlists/{id}/tracks request. For larger lists, split them into batches of 100 and add 1-second delays between each batch. Use PUT instead of POST when you want to replace all existing tracks in one operation (also limited to 100 URIs, so use POST for the rest).
Can RapidDev help me build a custom Spotify playlist automation?
Yes — RapidDev has built 600+ integrations and can set up a production Spotify curation pipeline with OAuth flow, scheduled curation, and a management dashboard. Reach out for a free consultation at rapidevelopers.com.
Does Spotify have webhooks so I can react to events in real time?
No — Spotify has no webhook system for the Web API. The only event-driven primitive is the Web Playback SDK's in-browser player events, which only work while the user is actively playing. For any background automation (new releases, updated playlists), you must poll on a schedule using cron or a task queue.
Need this automated?
Our team has built 600+ apps with API automations. We can build this for you.
Book a free consultation