Public APIs
All posts

Cursor Pagination: What Real API Cursors Contain

Quick answer: Cursor pagination replaces "give me page 3" with "resume from here". The API hands you a token and you send it back. Most real cursors are not opaque: we decoded live ones and found the last record's sort key in base64. That matters, because a cursor's contents decide what happens when you retry, resume, or change your query.

Everything written about cursor pagination explains it from the server's side: cursors are stable under inserts, offsets are not, use cursors. All true, and all useless when you are holding a token from someone else's API and your loop has just crashed on page 40.

So we opened some up. We run a directory of public APIs, and 233 of the 1,570 live APIs we list (14.8%) are keyless and CORS-enabled, so we can call them from a browser console with no signup. We pulled cursors from the ones that issue them and took them apart on 11 September 2026.

What is actually inside a cursor?

Four cursor-issuing APIs from our directory, and what their tokens decode to:

| API | Field | What the token really is | |---|---|---| | Crossref | message.next-cursor | base64 of 1027619182000,10.1016%2Fs1010-7940%2897%2901214-1: a timestamp and the last DOI on the page | | Archive.org | cursor | base64 of [{"identifierSorter":"20250602_20250602_0736"}]: the last identifier on the page | | Wiktionary and the rest of the Wikipedia family | continue.apcontinue | the next page title, in plain text: !'O!Kung | | GitHub | Link header, since= | the numeric id of the last repository on the page: since=371 |

Not one of these is opaque. Every one is the sort key of the last row you received, either bare or wrapped in base64 that any language decodes in one line. Our own earlier pagination survey called Wiktionary's token opaque. It is not, and the difference is practical rather than academic.

Because the MediaWiki cursor is a plain title, you can write one yourself. Asking for apcontinue=zebra returns zebra, zebra's and zebra-back. That is a cursor you can construct from your own last-seen record, which means a crashed job can resume without having stored anything the API gave it.

Is a next URL a cursor?

Usually not, and this is the mistake that quietly costs people records.

A response that hands you a ready-made next URL feels like cursor pagination. In our directory it almost never is. We called the five most-used next-URL APIs we list and read what the URL contained:

| API | What the next URL carries | |---|---| | SWAPI | ?page=2 | | Rick and Morty | ?page=2 | | PokéAPI | ?offset=3&limit=3 | | Spaceflight News | ?limit=3&offset=3 | | Launch Library 2 | ?limit=3&offset=3 |

All five are offset pagination wearing a friendly hat. Following the URL you were given is still the right move, because it carries parameters you did not send, but it buys you none of the stability of a real cursor. If a record is inserted near the front of the collection while you are on page 12, you will read one record twice, exactly as you would if you were counting offsets yourself.

The test is one line: look at the next URL. If it says page or offset, you have offset pagination and the drift that comes with it.

What happens when you send the same cursor twice?

We sent Crossref the same cursor twice and got back a byte-identical page of DOIs. That is the behaviour you want: the cursor is a pointer, not a ticket. A request that times out can be retried with the same token, and a job that crashes can resume from the last cursor it persisted.

Check it on any API you build a long loop against: the alternative design exists, and cursors that are consumed on use or expire after a short TTL force you to restart a walk rather than resume it.

Can a cursor be used with a different query?

This is the failure mode nobody writes about, and it is silent.

We took a cursor from a Crossref search for svg and sent it back with the query changed to quantum. The API answered 200 OK with a perfectly well-formed page of results, reporting 777,251 total results against the 16,080 the original search reported. No error, no warning. The cursor is only a sort position, so it applies itself happily to whatever result set you name.

If your loop builds its request from a variable and that variable changes mid-run, nothing will tell you. You will collect a plausible-looking dataset assembled from two different queries. Bind the cursor to the query in your own code, because the API will not do it for you.

How do you loop a cursor safely?

  1. Store the cursor with the query it came from. Treat them as one value. Where the token is plaintext, like MediaWiki's, store your last-seen record instead and rebuild the cursor on resume.
  2. Cap the loop on your own counter, not on the API's signals. We hit a live example of why while writing this: from our network, Archive.org's scrape endpoint returned the same 100 identifiers and the same unchanged cursor on every follow, and returned that same payload for a completely different query. That reads more like an edge cache in front of the API than the API itself, but a loop that trusts cursor != null would still be running now.
  3. Stop on a repeated cursor. One comparison against the previous token catches the case above before it becomes a bill.
  4. Rate limit yourself between pages. A cursor walk is the most common way a small script becomes an accidental flood. Our rate limiting guide covers what that looks like from the receiving end.

Cursor or offset: which should you use?

| If you need | Use | |---|---| | A stable walk over data that is being written to | Cursor | | To resume a crashed job from where it stopped | Cursor, and persist the token | | To let a human jump to page 40 | Offset or page numbers, because cursors cannot | | To know how many pages there are | Offset, since most cursor APIs never tell you | | To walk a slow-moving reference dataset | Either, and offset is simpler |

For anyone consuming public APIs, you rarely get the choice. Two thirds of the endpoints in our last pagination study returned no pagination metadata at all, and exactly one of 51 issued anything cursor-shaped. A cursor is something you handle correctly when you meet one, not something you can ask for. Every keyless API in our development category is available to practise on.

Frequently asked questions

What is cursor pagination?

Cursor pagination returns a token with each page that means "resume from here". You send the token back to get the next page, instead of asking for a page number or an offset. The token usually encodes the sort key of the last record you received, which is what lets the server pick up at exactly that point.

What is the difference between offset pagination and cursor pagination?

Offset pagination counts rows from the start of the collection, so any insert or delete before your position shifts everything and you skip or duplicate records. Cursor pagination anchors to a specific record, so edits elsewhere in the collection do not move your position. The trade is that cursors only go forward and cannot jump to an arbitrary page.

Is cursor-based pagination good?

For walking a large or actively changing collection, yes, and it is what every high-write API uses. For a paginated UI where people click page numbers, it is the wrong tool: you cannot compute a page count or jump to page 12 from a cursor.

Are pagination cursors encrypted?

Rarely. Of the four live cursors we decoded, none were: two were base64 of a sort key, one was a plain page title, one was a numeric id in a header. Base64 is an encoding, not a protection. Do not assume a cursor hides anything, and if you are building an API, do not put anything in one you would not return in the response body.

What happens if a cursor expires?

Behaviour varies, which is why you should test it before relying on it. Some APIs return an error you can catch and restart from; others accept the stale token and return results from a position that no longer means what it did. Where the cursor is a plaintext sort key you can sidestep the question entirely by rebuilding it from your own last-seen record.