Spotify Web API: OAuth PKCE, Scopes, Quotas, Rate Limits
Learn Spotify’s OAuth flows, a PKCE login with real URLs, least-privilege scopes, redirect URI rules, development vs extended quota mode, and handling 429s.
Spotify Web API authorization and limits: common questions
Which Spotify OAuth flow should a single-page or mobile app use?
Authorization code with PKCE. The app can’t keep a client secret, so a one-off code_verifier proves the app redeeming the code is the one that started the login. It still gets refresh tokens.
What should a client do when Spotify returns 429?
Wait at least the Retry-After seconds, back off with jitter, and cut calls with lazy loading and snapshot_id. A 429 with reason QUOTA_EXCEEDED means the development-mode quota is used up, not a short burst.
Related chapters
- Start here in System Design
- How a system design interview is graded
- The delivery framework: five phases and a clock
- Numbers to know: what one machine can really do
- System Design Concepts: CAP, SQL vs NoSQL, Message Queues
- SQL vs NoSQL: How to Choose a Database (System Design)
- Traffic Jumps 10×: What Breaks First? (System Design)
- Proxy vs Load Balancer vs API Gateway (System Design)
- CAP Theorem Explained Simply, with Examples
- Why Use a Message Queue? System Design Explained
- Message Queue vs Pub/Sub vs Kafka: Differences
- Polling vs Long Polling vs SSE vs Webhooks (System Design)
- API design in System Design
- REST vs GraphQL vs gRPC: Choosing an API (System Design)
- API Goals Canvas: Design APIs from the Consumer
- REST API Design: Resources, Methods, Status Codes
- HTTP Semantics, Richardson Maturity Model and HATEOAS
- GraphQL Explained: Over-fetching, Schema, N+1 Problem
- RPC and gRPC Explained: Protobuf, HTTP/2, When to Use
- API Design Best Practices: 9 Principles with Examples
- API Payload Design: Names, Types, Links and Errors
- Mapping Database Tables to API Resources, Step by Step
- API Design for Complex Schemas: Trees, Polymorphism, Bookings
- API Pagination, Idempotency Keys, Errors and Versioning
- Network-Efficient APIs: HTTP Caching, ETags, Field Selection
- Async APIs: 202 Accepted, Webhooks and Bulk Operations
- API Versioning and Evolution Without Breaking Clients
- API Contracts: OpenAPI, Contract Testing and Docs
- API Security: Authentication, Authorization, Rate Limiting
- OAuth 2, OpenID Connect and Scopes for APIs
- OWASP API Top 10 Explained: BOLA, SSRF and More
- Running APIs: Gateways, Mesh, Rate Limits, Canaries
- API Troubleshooting Interview: Slow APIs and Unexpected Errors
- API Design Review: A Staff-Level Capstone
- Spotify Web API: Resources, IDs, Payloads and Paging
- Spotify Web API Changes: Real API Evolution Lessons
- Database-heavy designs in System Design
- Design a Distributed Key-Value Store (System Design Interview)
- Design a Hotel Reservation System (System Design Interview)
- Design a Digital Wallet (System Design Interview)
- Design S3-like Object Storage (System Design Interview)
- Games in System Design
- Shard City: Learn Database Sharding by Playing a Game
- System Design Case Studies: Flash Sale, Chat, ChatGPT
- Flash Sale System Design: Who Gets the Last Item
- Food Delivery App System Design: Which Database?
- Cache Stampede: Why the Database Fell Over and the Fix
- Live Streaming System Design: Scaling for a Final
- ChatGPT System Design: Streaming LLM Responses
- RAG System Design: Product Q&A on Sale Day
- IRCTC Tatkal System Design: Booking Under Extreme Load
- Group Chat System Design: WebSockets and Fan-Out
- Authentication in System Design: Sessions, JWT, OAuth
- Authentication Methods Compared: Sessions, JWT, OAuth
- JWT in System Design: Sessions, Revocation, Refresh Tokens
- JWT Interview Questions and Answers
- Life of a session, step by step
- JWT Structure Explained: Header, Payload, Signature
- Forge lab: why verification must be strict
- JWT vs Session Authentication: Which to Use
- The validation pipeline: where a bad request dies
- How to Revoke a JWT: Blocklists and Short Expiry
- Refresh Tokens and Rotation Explained
- Logout in depth: one device, all devices, SSO
- JWT Security Best Practices
- OAuth 2.0 and OpenID Connect Explained
- Where to Store JWT: localStorage vs HttpOnly Cookie
- JWT in Microservices: API Gateway vs Service Validation
- Microservices, keys, and trust boundaries
- Sender-constrained tokens: making a stolen token useless
- Authentication vs Authorization Middleware
- Failure explorer: what breaks when X fails
- Capacity planner: sessions vs JWT vs hybrid
- Edge cases catalog
- Test it, measure it, migrate to it
- Anti-patterns to reject in a design review
- Design checklist and cheat sheet
Keep learning
Overview
Nightly server job reading public album data Client credentials No refresh token. The job just asks for a new token when the old one expires. No person is involved, so there is nobody to consent. The job authenticates as the app with client_id and secret. That is fine for catalogue data, but the token carries no user, so every /me endpoint is out of reach. Web app with its own backend Authorization code Yes. Store the refresh token server side, next to the user’s session. The server can keep the client secret safe, so it redeems the code with client_id and secret. Spotify recommends this for long-running server-side apps. Adding PKCE on top costs little. Single-page app or mobile app Authorization code with PKCE Yes. Keep it in the most protected storage the platform offers. Anything shipped to a browser or phone can be unpacked, so a “secret” inside it is public. PKCE replaces the secret with a one-off code_verifier, which protects the code if it is intercepted on the redirect. A CLI tool for your own account Yes, so you log in once and the tool refreshes quietly. Lab: which Spotify flow fits this app? Read my playlists Add playlist-read-collaborative only if you also list collaborative ones. Create a private playlist Send "public": false. A new playlist is public by default, which would need playlist-modify-public instead. Add tracks to it Same scope as creating it, because the playlist is private. Start playback on my phone Player endpoints also need the listener to have Premium. Show what’s playing Save tracks to my library Read my email Since February 2026, GET /me no longer returns email to development-mode apps, so ask whether you need it at all. Lab: least privilege for a “playlist maker” app Features the app has “Might be handy later” scopes Every extra scope is one more line on the consent screen, one more reason for a user to tap Cancel, and more damage if the token leaks. Ask for it on the day a feature needs it, by sending the user through authorization again with the wider scope list. This is the smallest set that makes the ticked features work. A leaked token can do no more than the app itself was meant to. Retry immediately Stuck in 429s t=0s GET /me/playlists → 429 Retry-After: 4\nt=0s GET /me/playlists → 429 (retry)\nt=0.1s GET /me/playlists → 429 (retry)\nt=0.2s GET /me/playlists → 429 (retry)\n… every retry is another call in the same window Every retry lands inside the same rolling 30-second window, so it counts too. The client keeps the window full and never recovers. Honour Retry-After Recovers, then bunches up Much better: the client waits as told. But every queued request wakes at the same moment and refills the window, so the 429s come back in waves. Retry-After + exponential backoff with jitter Smooth recovery Wait at least Retry-After, then spread retries out with a growing, randomised delay so many clients (or many tabs) don’t retry in step. Backoff + cache + lazy load Fewest calls in the first place Only call when the user acts, keep each playlist’s snapshot_id and skip refetching when it hasn’t changed, and use batch endpoints. Backoff stays as the safety net, and rarely fires. Lab: a client sends a burst and gets a 429. Pick its strategy. Simulation: timings and Retry-After values are made up. Spotify does not publish its limit.
Spotify Web API II: OAuth scopes, quotas and rate limits
The security chapters used a made-up ticket site. Here the same ideas meet a real API that millions of apps call: which OAuth flow to pick, the exact URLs of a PKCE login, scopes, redirect rules, quota tiers, 429s, and the access-control choices Spotify made.
Three flows, three kinds of app
listed many OAuth grants. Spotify implements three of them, and the choice depends on one question: can your app keep a secret, and is a user involved? Authorization code Authorization code + PKCE browser, mobile, desktop For apps that can’t store a secret. A one-off code_verifier proves the app that redeems the code is the one that asked. Refresh tokens: yes. Client credentials daemons, backend jobs The app authenticates as itself, no user. Only endpoints that don’t touch user information, such as catalogue data. Refresh tokens: no. The last card is the usual surprise. A client-credentials token has no user behind it, so GET /me or GET /me/playlists can’t work: there is no “me”. It is the machine-to-machine case from the security chapter , where an API key identifies a program, not a person.
A PKCE login with Spotify’s real URLs
This is the flow from the OAuth chapter Your app verifier and state stay in the app GET /authorize?response_type=code&client_id=…&code_challenge_method=S256&code_challenge=…&scope=…&state=…&redirect_uri=… app sends the browser to Spotify user logs in and sees the scopes on the consent screen redirect → redirect_uri?code=…&state=… (or ?error=access_denied&state=…) the code is the only thing that crosses the browser state matches? POST /api/token with code + code_verifier direct HTTPS call, no browser SHA-256(verifier) matches the challenge → access_token (1 hour) + refresh_token GET /v1/me/playlists Authorization: Bearer … The app checks state on the callback (a mismatch means someone else started this login, the login CSRF from the OAuth chapter), then redeems the code. No client secret: the verifier does that job. Auth errors (accounts.spotify.com) Invalid client secret API errors (api.spotify.com)
Scopes: the catalogue, and asking for less
Spotify’s scopes follow the advice from the OAuth chapter : grouped by area, and split into read and modify so an app can ask for one without the other. playlist-read-private · playlist-read-collaborative · playlist-modify-private · playlist-modify-public user-library-read · user-library-modify Spotify Connect user-read-playback-state · user-modify-playback-state · user-read-currently-playing app-remote-control · streaming (Web Playback SDK, Premium) Listening history user-read-playback-position · user-top-read · user-read-recently-played user-follow-read · user-follow-modify user-read-email · user-read-private · user-personalized A handful of soa-… and user-soa-… scopes also exist, but they are partner-only. Now build an app and see the smallest set it needs: : the API still checks the object belongs to the token’s user.
Redirect URIs: exact, HTTPS, and no localhost
The redirect URI is where the authorization code lands. Spotify tightened the rules in 2025: HTTPS, registered in the dashboard loopback IPv4: HTTP is permitted loopback IPv6: HTTP is permitted “localhost is not allowed as redirect URI” plain HTTP on a real host not an exact match of what was registered April 9, 2025 enforced for newly created apps November 2025 all clients expected to have migrated Why so strict? The redirect carries the code. If an attacker can make it land on a page they control (a loose prefix match, an open redirect on your domain, plain HTTP on a café network), they hold the code. The OWASP chapter
Quota modes: Spotify’s pricing tiers
separated rate limits (protect the servers, per second or minute) from quotas and plans (what a customer is allowed overall). Spotify’s version has no price tag, but it has two tiers: Development mode Extended quota mode Who starts here every new app by application only up to 5, each on an allowlist Owner needs Premium yes (since Feb 2026) not affected by Feb 2026 changes Request limits rate limit + per-developer quota much higher rate limit Read other users’ profiles and playlists Who may apply organizations only (since May 15, 2025) Two failures look different on the wire. A user who isn’t on the allowlist can log in, but their API calls are refused: The reason field matters: a plain 429 means “slow down for a moment”, while QUOTA_EXCEEDED is about the developer’s overall allowance, separate from the rate limit, so retrying it quickly just burns more calls. The February 2026 changes applied to new development-mode apps from February 11, 2026, and existing ones were migrated on March 9, 2026. Getting out of development mode is a business review, not a form: since May 15, 2025 only organizations can apply, and the criteria include an established legal entity, a launched and active service, at least 250,000 monthly active users, availability in key Spotify markets and commercial viability. The review can take up to six weeks. In plan terms: a free sandbox for building, and a partner tier for products.
Rate limits: a rolling window you can’t see
Spotify counts each app’s calls in “a rolling 30 second window”. Go over and you get 429, which “will normally include a Retry-After header with a value in seconds”. The number itself is not published, which is a deliberate choice: clients must react to the signal, not plan around a figure. Same idea as the 429 in the security chapter , from the caller’s side. Spotify’s own advice maps onto the last two buttons: back off and retry after Retry-After, use batch endpoints where they exist, load lazily, and keep each playlist’s snapshot_id so you can skip fetching playlists that haven’t changed. Spotify: limit hidden No remaining-calls header. You learn you are over only from the 429 and its Retry-After. Clients must be defensive all the time. GitHub: limit visible x-ratelimit-remaining and x-ratelimit-reset (UTC epoch seconds) on responses, plus retry-after when you must wait. A conditional request answered 304 doesn’t count against the primary limit if authorized, and GitHub asks for serial rather than concurrent requests to avoid secondary limits, and webhooks rather than polling. The GitHub column is what argued for: make 304 cheap and clients start sending If-None-Match. Spotify’s snapshot_id is a do-it-yourself version of the same validator.
Access control lessons from Spotify
“Public” is visibility, not access A playlist’s public flag decides whether it shows on the profile and in search. Anyone with the link can still open a non-public one, and a collaborative playlist lets anyone with the link edit. A playlist can’t be public and collaborative at once. Entitlement checks Player endpoints work only for Premium users. A valid token and the right scope are not enough: the account’s product tier is a third check. Allowlists for sandboxes GET /me/playlists takes the user from the token. There is no user id in the URL for an attacker to change, so the BOLA bug from the OWASP chapter has nowhere to start. That last card is the design move to copy. happens when /users/77/… trusts the 77. Spotify’s February 2026 development-mode changes pushed even further toward /me: development mode now GET /me GET /me/playlists POST /me/playlists GET /me with country, email, explicit_content, followers, product GET /me without those fields , done at the tier level.
The interview line
“For a Spotify integration I’d use authorization code with PKCE for the browser or mobile client, plain authorization code if a backend holds the secret, and client credentials only for catalogue jobs, since that token has no user. I’d ask for the minimal scopes, register an exact HTTPS or 127.0.0.1 redirect URI and check state. Tokens last an hour, so the client refreshes and keeps the old refresh token if none comes back. On 429 it waits for Retry-After with jittered backoff, and treats reason QUOTA_EXCEEDED as a quota problem, not a retry problem. Calls go through /me, so there’s no user id to tamper with.” Everything here was a snapshot of rules that keep moving: redirect rules in 2025, quota criteria in 2025, development mode in 2026. The next chapter looks at that movement directly: how Spotify’s API changed in 2024 and 2026, what it removed and why, and what other companies’ APIs show about evolving in public.
More courses
- Jobs, linked to lessons — Browse jobs where every requirement links to the chapter that teaches it. See how ready you are, how long the gap takes to close, and which listings look stale.
- Match a job post to courses — Paste a job post. Each skill is matched to a track here and put in learning order, foundations first.
- React from open source with patterns — Patterns extracted from Material UI, Radix, React Aria, React Hook Form, Redux Toolkit, TanStack, and Next.js — not slides about hooks.
- Database in depth — How a query becomes work. Indexes, isolation, locks, and the plans that explain why a page is slow — not an ORM tour.
- Build a real API with NestJS — One ticket-booking API, built in order: a plain Node server first, then NestJS modules, validation, a real database, and the features real clients lean on.
- Microservices — Microservice patterns one diagram at a time: how to split a monolith, how services talk, and how data stays consistent without distributed transactions.
- Kafka — A beginner-to-senior path through event logs, reliable delivery, production operations, and disaster recovery.
- AI engineering for software engineers — Eight free reads, in order, for software engineers moving into AI: when to build an agent, how to build one, how to test it, and how the model works underneath.
- Frontend system design — How a UI is sliced when it is a product, not a page: ownership of state, cache, routes, and what happens when the network lies.
- TypeScript in depth — The type system as a design tool. Inference, narrowing, generics, and the errors that mean the model is wrong — not a catalog of utility types.
- CSS in depth — See the box, then the page. Visual chapters from the box model through Flexbox surprises, Grid, selectors, and units, then backgrounds, shapes, and visual effects, each with a playground.
- Accessibility — Foundations first (WCAG in plain words, semantic HTML, lists, alt text), then hands-on HTML labs, a full pass of WAI-ARIA 1.3, and a final fix-the-page challenge.
- Docker — What an image is, why layers cache, and how containers find each other on a network.
- Redis — Fifteen Redis patterns, one picture each: cache, session, lock, counter, rate limiter, queue, and when Redis is the wrong tool.
- Kubernetes — Kubernetes from zero to operators: why it exists, Pods, Deployments and Services, then configuration, networking, storage, scheduling, scaling and security.