Spotify Web API Changes: Real API Evolution Lessons
Spotify’s 2024 and February 2026 Web API changes, GitHub date versions, Meta path versions, Stripe idempotency keys, signed webhooks and GraphQL cost limits.
API evolution in real public APIs: common questions
How did Spotify change its Web API without a new version?
It kept /v1 and changed the contract by quota tier: development-mode apps got renamed endpoints and removed fields on published dates, while Extended Quota Mode apps stayed unchanged.
How does GitHub version its REST API?
With a date-named X-GitHub-Api-Version header, defaulting to 2022-11-28. Breaking changes ship only in new versions, the previous one stays supported for at least 24 months, and unsupported versions return 410 Gone.
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: OAuth PKCE, Scopes, Quotas, Rate Limits
- 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
Same operation, new path. Old code calls a path that is gone for development-mode apps. This is the textbook case a /v2 would have handled: keep the old path in v1, ship the new one in v2. Keep every Spotify path in one module, so a rename is a one-line change and a contract test fails before users do. Playlist field tracks is renamed items (tracks.tracks.track → items.items.item) A rename is a remove plus an add. A parser that reads playlist.tracks.total gets undefined. A version bump exists for exactly this. Map the JSON into your own type in one adapter. Read items, fall back to tracks while both kinds of app exist, and treat “absent” as normal: for playlists you don’t own or collaborate on, items is not there at all. Search: limit max 50 → 10, default 20 → 5 Same URL, same fields, smaller pages. A client that assumed one call returns 50 results now shows 5 or 10. Behaviour is part of the contract, so this breaks clients even though no field moved. Never assume a page size: read the paging object and follow offset until you have what you need, within your rate limit. Track popularity and Artist followers are removed Code that does track.popularity.toFixed() throws. A new version would only delay this: the data is being withdrawn on purpose, so it would be missing in v2 as well. Null-safe parsing: every optional-looking field gets a default, and the UI hides what is missing instead of crashing. Not a shape change but a product decision. No version number brings back a feature the provider no longer offers to this kind of app. Feature-flag everything built on one endpoint, so you can switch it off cleanly on the migration date. A version would keep old code compiling, but the replacement costs N calls instead of one, which bites against rate limits. The guide itself ends with “Limit API call volume to avoid rate limits”. PUT /me/tracks, /me/albums, /me/following … become PUT /me/library with Spotify URIs Seven type-specific endpoints fold into one that takes URIs, so the URI’s type says what you are saving. Pure reshaping: a classic major-version change. Store and pass spotify:track:… URIs rather than bare IDs plus a type you track yourself. Code built on URIs needed only a path change. An Extended Quota Mode app keeps calling PUT /me/tracks after March 9 2026 The guide says for extended apps “All existing endpoints, fields, and behaviors remain unchanged”. Adopting /me/library is optional for them. Same /v1, different contract per tier. Know which tier your app is in, and read the migration guide for both. If you plan to move tiers, test against the stricter one. Lab: eight real changes. You run a development-mode app. Breaking for an existing development-mode client? Not breaking Would a version bump (/v2) have helped? Spotify: one /v1, migrations per tier GitHub: date in a header Meta: version in the path The endpoint disappears for the affected tier on a published date. If you are in that tier, the feature is gone, whatever you pin. Additive changes such as a new response field or optional parameter are available in all supported versions. Pinned or not, you just see the extra field. Removing an operation is breaking, so it happens only in a new version. Once your pinned version is no longer supported, calls get 410 Gone: a loud, clear failure rather than a silent shape change. Your path decides which version you get, so read the changelog for each version before you move the number up. Your /v21.0 calls keep the old behaviour while that version lives: each version is guaranteed for at least two years. Then expired-version calls fall forward to the next oldest usable version, so plan the move before that date. Same lifecycle. Calls with no version in the path use the version set in the app dashboard, which is why Meta recommends always naming the version. Lab: pick a versioning strategy and a change. What does an existing client experience? Add a field Rename a field Remove an endpoint A normal delivery Body edited on the way The same delivery, sent again Forged by someone without the secret Reject before parsing. The signature does not match the raw body, so either the bytes changed or the sender never had the secret. Signature is valid, because it is a real delivery. The delivery ID is already in your store, so answer 2xx and skip the work. Without this check a resend runs your side effects twice. Signature matches and the delivery ID is new. Store the ID, queue the work, answer 2xx quickly. Lab: your webhook receiver checks four deliveries seen 2 minutes ago matches (constant-time compare) not reached already processed first time Signatures are illustrative strings, not real HMAC output.
Spotify Web API III: how a real API changes, and what it leaves out
Chapters 19 and 20 read Spotify’s API as it is. This one watches it change: two rounds of removals and renames, done without ever leaving /v1. Then it fills the gaps Spotify’s public API doesn’t show, using GitHub, Stripe and Meta, and ends with a map from every chapter of the course to a real API.
November 27, 2024: endpoints removed by audience, not by version
Spotify’s post “Introducing some changes to our Web API” made these unavailable to some apps overnight: Related artists Audio features Audio analysis Featured playlists Category playlists 30-second preview URLs in multi-get responses (SimpleTrack) Algorithmic and editorial playlists the ones Spotify owns New apps, development-mode apps without a pending extension request, and new use cases. “Applications with existing extended mode Web API access that were relying on these endpoints remain unaffected.” The stated reason was “creating a more secure platform”. Notice what did not happen: no /v2, no deprecation period for the affected group. The same URL now answers differently depending on who calls it. Lesson: a provider can retire a feature by audience instead of by version number. Evolving an API
February 2026: the Development Mode migration
Feb 11, 2026 Mar 9, 2026 5 users Extended Quota Mode apps New apps were also limited to one Client ID per developer (a July 2026 changelog raised that to 25). The API changes themselves, from the migration guide: after (development mode) save / follow PUT /me/library with Spotify URIs (same for DELETE) contains checks GET /me/tracks/contains and friends GET /me/library/contains playlist items batch fetch GET /tracks?ids=…, also albums, artists, episodes, shows, audiobooks, chapters browse and more removed, no replacement other users GET /me, GET /me/playlists, POST /me/playlists limit max 50, default 20 fields removed fields renamed playlist tracks, tracks.tracks, tracks.tracks.track external_ids was on the removed list for tracks and albums, then reverted in March 2026: even the migration guide changed after publication. One playlist call, before and after: After (development mode)
Lab: was it breaking, and would a version have helped?
Evolving an API had you judge twelve changes on a fictional site. Now judge eight real ones. Answer both questions, then read the reasoning and the pattern that would have protected your app. Tolerant reader Missing is normal Every field may vanish. Default it, don’t crash. The guide’s own checklist says “Handle missing removed fields safely”. One adapter URIs, not types Code that passes spotify:album:… to one generic endpoint survived the library merge.
The deeper lesson: Spotify versions by quota tier
Extended Quota Mode Big, reviewed apps. “All existing endpoints, fields, and behaviors remain unchanged.” They may adopt /me/library when they choose. Development Mode Small and new apps. Get the new contract on a published date: Feb 11 for new apps, March 9 for existing ones. Two contracts live behind one /v1 at the same time. That is a form of the “two in production” pattern from Evolving an API , except the selector is the app’s tier, not a number in the request. Removed fields stay documented in the schemas as deprecated, and each round comes with a dated migration guide. Old contract still served Client chooses when to move End date published Two in production (v1 + v2) Limited lifetime guarantee Spotify, extended tier Spotify, development tier ✓ yes · ~ partly · ✕ no. For development-mode apps it is closest to aggressive obsolescence with a dated guide. What a consumer of any such API should do: Contract tests assert the few fields you use, in CI Null-safe parsing defaults for every field Watch the changelog blog, migration guides, dates in the calendar Monitor in production alert on new 4xx and missing fields The same toolbox as , used from the client’s side.
What Spotify doesn’t do: explicit versions beyond /v1
Spotify’s whole public API sits under one prefix. Two big providers take the opposite route and put a version in every request: GitHub: a date in a header Meta Graph API: the version in the path The patterns chapter
Idempotency keys in a real API: Stripe
The patterns chapter built a retry-safe Pay button with an Idempotency-Key. Stripe’s API is where that design is documented in detail: All POST requests in API v1 accept the header. Don’t send keys on GET or DELETE: those are idempotent already. The key Same key, other body Reusing a key with different parameters returns an error, so a bug that recycles keys is caught. What is saved What is not saved Requests that fail validation or conflict with a concurrent request with the same key are not saved, so they can be retried. Why it matters A lost reply on a payment is no longer a double charge. The client retries without fear. Spotify PUT /me/library Saving a track that is already saved leaves it saved. PUT is idempotent by design, so a retry needs no key. , tone: 'warn', body:
Webhooks: GitHub and WhatsApp push, Spotify makes you poll
Spotify’s Web API has no webhooks. To know what is playing, a client polls GET /me/player. GitHub’s best practices say the opposite for its own API: subscribe to webhooks instead of polling. Long jobs, events and bulk GitHub delivery WhatsApp Cloud API (Meta) GitHub still sends a legacy X-Hub-Signature header, but recommends the SHA-256 one. Both providers sign the raw body, so verify before you parse JSON. GitHub’s signature does not include a timestamp, so a captured delivery re-sent later still verifies: the delivery ID you already stored is what stops it running twice.
202 Accepted in the wild
Long jobs, events and bulk answered a slow request with 202 and a job to poll. GitHub’s repository statistics endpoints do a lighter version of it: No job URL, no Location header: the resource itself is the thing you poll. Spotify lists 202 Accepted in its status-code table, but its player commands are fire-and-forget: the docs warn that “The order of execution is not guaranteed when you use this API with other Player API endpoints.” A client that sends pause then next should read GET /me/player to learn what actually happened.
GraphQL and OpenAPI for real: GitHub
The GraphQL chapter warned that one query can ask for a huge tree. GitHub’s GraphQL API shows the guard rails a public GraphQL API needs: first or last on every connection max total nodes one call may request points per hour (as documented for GraphQL v4) points for the query above: 1 + 100 + 10,000 requests, ÷ 100 Running APIs made about quotas. GitHub also publishes an OpenAPI description of its REST API in the github/rest-api-description repository: the real-world version of the single source of truth in , from which clients, mocks and docs can be generated.
The whole course, mapped to real APIs
where to see it for real Consumer goals Spotify’s /me/… resources: designed around “the signed-in user”, not around Spotify’s tables. HTTP semantics GitHub’s GraphQL API with first/last and point costs. RPC and gRPC Spotify’s player commands read like remote calls: PUT /me/player/pause, POST /me/player/next. For gRPC proper, many Google Cloud APIs are offered over both gRPC and REST. GitHub’s github/rest-api-description OpenAPI files. OAuth and scopes Spotify’s authorization code with PKCE and its scope list ( chapter 20 OWASP API Top 10 Spotify’s 2026 development mode returns fewer user fields and no other users’ profiles: less data exposed, the theme of API3. Design review Review Spotify’s February 2026 migration guide itself: what broke, who paid, what a version would have changed.
The interview line
“Real APIs change by more than version numbers. Spotify kept one /v1 and changed the contract by quota tier with dated migration guides, so as a client I parse tolerantly, treat every field as optional, keep paths in one adapter and watch the changelog. As a provider I’d add what Spotify doesn’t show: dated versions like GitHub’s with a two-year promise, Stripe-style idempotency keys on POST, signed webhooks instead of polling, and cost-based limits if we offer GraphQL.” That closes the course: from picking a protocol to reading how a real API grows, breaks and recovers.
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.