Skip to main content
The API pages with cursors, and it uses four different cursor dialects. The field name, the type, and the bootstrap behaviour all change depending on which family of operations you are calling. Get the dialect wrong and you get a 400 or a silent empty page. Read this table before writing a paging loop.

The four dialects

Three things to notice:
  1. Library is camelCase. Feeds is snake_case. nextCursor and next_cursor are never interchangeable. Sending cursor to a feeds operation that wants next_cursor back is fine — the request field is called cursor in both families — but reading the wrong response field silently yields undefined and your loop stops after one page.
  2. Content cursors are numbers, not strings. POST /v1/articles/read returns nextCursor: 47. That is a paragraph anchor. GET /v1/library/items/{id}/content returns a character offset. Both are integers and both are validated as integers — passing a string is a 422.
  3. cursor: "latest" is a bootstrap, not a page. It sets a checkpoint and returns no events. See below.

The absolute rule

A cursor is opaque. Never construct one. Never parse one. Never hand one to a different request than the one that produced it.
Concretely, all four of these are errors the server detects and rejects:
  • Constructing a cursor. The string dialects are base64url over an internal structure. That structure changes. Decode it and your client breaks on a deploy.
  • Reusing a cursor across a changed predicate. GET /v1/library/items binds its cursor to the view you paged with. Switching from view=all to view=history mid-loop returns invalid_request: “Choose a valid library page cursor.”
  • Reusing a feeds cursor with a different where. POST /v1/feeds/changes fingerprints the predicate into the cursor. Change where and you get: “This cursor belongs to a different where predicate. Restore the original where, or restart with cursor:"latest".”
  • Acknowledging a cursor from another consumer. POST /v1/feeds/consumers/{id}/ack rejects stale, foreign, and invented cursors outright.
The numeric content cursors look guessable. They are not a licence to guess: pass back the exact nextCursor you were given. Skipping ahead to an anchor you invented returns a window you did not mean to read. When you change a filter, start over. Drop the cursor, re-issue the first call, and let the server hand you a fresh one. See Errors for the codes these failures arrive with.

Paging the library

Loop while nextCursor is non-null. The page size is capped at 100 regardless of what you ask for.

Response

200 OK
The last page returns "nextCursor": null. That is the only stop condition — never infer the end from an item count, because a page can come back short and still have more behind it.

Paging the feeds

POST /v1/feeds/changes is the polling primitive, and it is where the latest bootstrap lives.
cursor: "latest" returns zero events. It only establishes the checkpoint. You must poll again with the returned next_cursor to receive anything.
The sequence is: bootstrap once, store the cursor, then poll forever.

Response

The bootstrap call and a drained poll look the same except for the cursor:
200 OK — the bootstrap
Store next_cursor and poll again. The second call returns the events discovered since.
Persist the cursor before you act on the events, or after — but pick one and be consistent. Persisting after means a crash redelivers the batch; persisting before means a crash drops it. For at-least-once delivery without writing your own state, use a managed consumer and let smry hold the checkpoint. See Retries and idempotency.

Paging article content

POST /v1/articles/read walks a long article in token-budgeted windows. The cursor is the anchor to resume from.
curl
200 OK
Stop when nextCursor is null. Each continuation is a separate operation against the account budget, but a continued read hits the cache, so it costs no fresh extraction. See Rate limits.
Do not page an entire article by default. POST /v1/articles/search returns only the passages that answer a question, usually for a fraction of the tokens. Page with POST /v1/articles/read when you genuinely need continuous text.

Checklist

1

Match the dialect to the family

Library reads nextCursor. Feeds reads next_cursor. Content cursors are integers.
2

Freeze the predicate for the whole loop

Filters, view, and where must be byte-identical across every page. GET /v1/library/items only checks view: change status, tag, or query mid-loop and the cursor is accepted, then pages one result set with another set’s position. POST /v1/feeds/changes does check the whole where and rejects a mismatch.
3

Stop on null, never on a count

A short page is not the last page.
4

Persist the cursor, not the page number

A crash resumes from the cursor. There is no page number to resume from.
5

Restart clean when the filter changes

Drop the cursor and re-issue the first call. Reuse is an error, not a shortcut.

Next

Retries and idempotency

Safe retries for saves, mutations, and managed consumer batches.

Errors

The codes a rejected cursor arrives with, and what to do about each.
Last modified on September 13, 2026