API Pagination, Idempotency Keys, Errors and Versioning

Offset vs cursor pagination, Idempotency-Key headers, error shapes and versioning, compared on real APIs: Stripe, the Spotify Web API and the Spotify Ads API.

API pagination, idempotency and versioning: common questions

Why use cursor pagination instead of offset?

Offset skips a count of rows, so a row added or removed while the user scrolls makes items repeat or vanish, and deep pages get slow. A cursor says “after this item”, so it stays correct and fast at any depth.

What is an idempotency key?

A random ID the client creates once per action and sends in a header on every retry. The server stores each key with its result and returns the stored result for a repeat, so a retried payment is not charged twice.

Related chapters

Keep learning

Overview

Lab: a new event appears while you scroll Offset paging Cursor paging Page 1 (you already see this) 0.3rem 0 0 Not loaded yet. Step 1: someone adds Event 9 at the top Step 2: scroll down, load page 2 Nothing changed between the pages, so offset worked. Try step 1 first. Lab: the reply gets lost, the app retries Without a key With Idempotency-Key 1. Tap “Pay” 2. No reply… app retries The user paid twice for the same seats. However many times the app retries, the user pays once.

Pagination, idempotency keys, errors and versioning

The nine principles said lists must be paged, retries must be safe, errors must be helpful and changes must not break old apps. Here is how each of those actually works, first on our ticket site, then in real APIs: Stripe, the Spotify Web API, the Spotify Ads API and LinkedIn’s Marketing API.

Pagination: one page at a time

GET /events on a big ticket site could match two million events. Sending all of them would be slow for the server, the network and the phone. So the API returns a page, say 20 events, plus a way to ask for the next 20. There are two common ways to say “the next 20”: Offset: “skip 40, give me 20” GET /events?offset=40&limit=20Easy, and you can jump to page 50. But the database still walks past the 40 skipped rows, and the list can shift under you. Cursor: “after this one, give me 20” GET /events?cursor=evt_456&limit=20The server sends back a cursor (a bookmark) with each page. Fast at any depth, and new rows don’t shift it. You can’t jump to page 50. What “the list can shift” means is easiest to see. The list below is newest first and pages hold 4 events. Run both steps with offset, then switch to cursor and run them again:

How real APIs do it

The same two ideas, four flavours. Click through and compare how each one says “the next page” and “that was the last page”: Cap the page size Stripe: 1–100, default 10. Spotify Ads: up to 50. Spotify search: 0–10. A cap protects the database from one greedy call. Make cursors opaque Stripe v2 page tokens and Spotify Ads continuation tokens are encrypted strings. Clients cannot edit them, so you can change what is inside later. Hand out the next link Spotify’s next and Stripe v2’s next_page_url are ready-made URLs. Clients follow them instead of building URLs, and null means done. Total is optional Freeze the filters Pick per endpoint Spotify Ads uses offset for small campaign lists and tokens for big reports. One API can mix both, as long as each list is consistent.

Filtering and sorting

Filters and sort order are optional, so they go in the query ( the REST chapter ). Pick one naming style and reuse it on every list: Read it as: events in Mumbai, in November, newest date first (the - means “descending”), 20 at a time. If /bookings also supports ?sort=-date the same way, developers only learn it once.

Idempotency keys: making POST safe to retry

The REST chapter said POST is not safe to repeat. Here is why that hurts. The user taps Pay. The server books the seats and charges the card, but the reply is lost in a tunnel. The app sees no answer and sends the same request again. The server can’t tell it is a retry, so it books and charges again. The fix: when the user taps Pay, the app makes up a random ID once (an idempotency key) and sends it in a header with every attempt. The server saves each key with its result. If a key comes back, it returns the saved result instead of doing the work again. Stripe’s payment API works exactly like this. Two details: a new tap on Pay gets a new key (it is a new purchase), and saved keys can expire after a day or so, since retries happen within seconds.

How real APIs answer a repeated key

All three use the same Idempotency-Key header, but they answer a repeat in different ways. Pick a situation: What the server has to store for each key, whichever style you choose: Key + who sent it Scope keys to the account or API client. Two customers may both pick “order-1001”. A fingerprint of the request Method, path and a hash of the body, so “same key, different body” can be rejected (Stripe’s idempotency_error, Spotify’s IDEMPOTENCY_KEY_REUSED). Status: running or finished A second request that arrives while the first is still running must wait or get “in progress”, never run in parallel. The result and an expiry The saved response (or the id of what was created) and a time to forget it: 24 hours at Stripe v1, 7 days at Spotify Ads, 30 days at Stripe v2.

Errors with a fixed shape

The status code says what kind of problem it was ( the REST chapter The payload chapter shows the standard application/problem+json shape. The body should say exactly what went wrong. Use the same shape for every error so apps can handle all of them with one piece of code: Seat A1 was booked by someone else. Pick another seat. For programs. Never changes, so the app can check if (code === "SEAT_UNAVAILABLE") and reload the seat map. For humans. Says what happened and what to do next. Can be reworded any time. Which field or item was the problem, so the app can highlight it.

How real APIs shape errors

Two habits worth copying. Stripe adds a type above the code (card problem, bad request, idempotency problem, server problem), so apps can handle families of errors before single codes. Spotify Ads puts a trace id in every response, so a user’s bug report points to one exact request in your logs. Add a request_id to your shape for the same reason. The most useful thing an error tells a client is whether to try again:

Versioning: changing the API without breaking old apps

Some changes break old apps: removing a field, renaming it, or changing what it means. When you truly need one, release it as a new version and keep the old one running until old apps are gone. Two common ways to say which version you want: In the URL GET /v1/events → GET /v2/eventsEasy to see and test. The most common choice. In a header API-Version: 2026-10-01Keeps URLs clean. GitHub and Stripe send a date like this. The hard part is knowing which changes are breaking. Try these before reading on: Adding new fields or new endpoints does not need a new version. Old apps simply ignore fields they don’t know. Evolving an API goes deeper: what counts as breaking, semantic versions, and how to retire an old version.

In the interview

Next: making the API cheap on the network, with caching, ETags and letting clients pick what they need. Spotify’s paging object and the 2026 search caps (limit 0–10, offset up to 1000). Paging on Spotify Stripe’s Idempotency-Key, the model this chapter follows. Stripe idempotency

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.