Skip to main content
Nearly every list endpoint pages with an opaque cursor. There is no offset and no page number: read has_more, send next_cursor back as cursor, and stop when has_more is false. Three list routes are the exception and never page. Search content, Webhooks, and Webhook events return one bounded page, with has_more always false, no cursor in the response, and no cursor parameter to send.

The list envelope

Page size

limit is a whole number. On every list route a value outside the bounds is clamped into them rather than rejected: limit=500 returns the maximum and limit=0 returns one row, with no error. The X-Effective-Query response header reports the limit the page used, so read it when you want to know what a clamped request became. A limit that is not a whole number answers 400 with error.reason invalid_query. For a route that is not in this table, read its endpoint page. The parameter list there states that route’s own limit default and maximum.
URL-encode the cursor when you send it back. Some routes return a cursor that contains a timestamp, and its + and : characters do not survive a query string unencoded.

Treat the cursor as an opaque string

Store it and send it back unchanged. Do not read it, parse it, build one, or branch on what is inside it. The format differs by route and can change in any release. A cursor only works on the route that returned it, and on that route’s aliases. Sending it anywhere else answers 400 with error.param set to cursor.

What ties a cursor to a request

Some cursors carry only the position of the last row, so they keep working for as long as you hold them. Others are tied to the filters you sent or to a ranking that 0xinsider rebuilds on a schedule, and those stop working when either changes.

Handle a 400

A request with a cursor answers 400 with error.code bad_request in two cases. Branch on error.reason to tell them apart. cursor_expired carries no Retry-After header and no error.retry_at. Waiting changes nothing, so do not sleep and retry the same cursor; only a fresh first page helps.

Read every page

Both versions start over from the first page when a cursor expires, and give up after three attempts. Send the same filters on every page of one run, because changing a filter part way through either expires the cursor or reads a different set of rows.

What this does not tell you

  • A row count on most routes. total is only there on the routes that run a count.
  • A stable second page once a ranking is rebuilt. That is the cursor_expired case above.
  • How to jump straight to page 10. There is no offset, so you have to read every page before it.