Skip to content
Sursa Developers

Pagination

Lists of the Sursa API are paged with a cursor: you ask for up to limit items, and the response tells you how to get the next page. You never compute offsets, and items do not repeat or go missing between pages when data changes in the meantime.

ParameterValue
limitHow many items per page, usually 1 to 200. New lists default to 50; the reference shows the range and the default of each list.
cursorThe cursor from the previous page. Leave it out for the first page.

A list returns its next cursor in one of two ways, depending on the shape of its response. The page of each endpoint in the API reference shows which one it uses.

Lists with a data envelope

Lists added for the public API, such as the event feed, return an object with the items in data:

curl "https://api.sursa.com/v1/webhook-events?limit=2" \
  -H "Authorization: Bearer $SURSA_API_KEY"
{
  "data": [
    {"id": "evt_8a1f…", "type": "card.published", "created_at": "2026-10-24T10:00:00Z"},
    {"id": "evt_8a20…", "type": "card.updated", "created_at": "2026-10-24T10:00:03Z"}
  ],
  "next_cursor": "eyJ2IjoxLCJrIjpbMTA0Ml19",
  "has_more": true
}

When has_more is true, request the next page with cursor set to next_cursor and the same filters:

curl "https://api.sursa.com/v1/webhook-events?limit=2&cursor=eyJ2IjoxLCJrIjpbMTA0Ml19" \
  -H "Authorization: Bearer $SURSA_API_KEY"

Lists that existed before cursors and return an object keep their own envelope (for example {"items": […], "total": 120}). They get next_cursor and has_more in the same object; the items stay where they were.

Lists that return an array

A few lists return a bare JSON array, for example GET /v1/collections. Their response shape cannot change, so the next page comes in headers instead:

curl -i "https://api.sursa.com/v1/collections?limit=50" \
  -H "Authorization: Bearer $SURSA_API_KEY"
HTTP/1.1 200 OK
Link: <https://api.example.com/v1/collections?limit=50&cursor=eyJ2IjoxLCJrIjpbNTBdfQ>; rel="next"
X-Next-Cursor: eyJ2IjoxLCJrIjpbNTBdfQ

[{"id": 1, "name": "support", …}, …]
  • Link with rel="next" is the full address of the next page; X-Next-Cursor is the same cursor on its own.
  • No Link with rel="next" and no X-Next-Cursor means this is the last page.
  • Without limit, these lists return every item in one response, as they always did. Pass limit to page them; the cursor remembers the page size, so the next request may leave limit out.
  • GET /v1/cards is the exception: it always returns pages, 200 cards by default and up to 1000 with limit, so follow X-Next-Cursor even without limit.
// Every item of a list that returns an array.
async function* listArray(path) {
  let next = `https://api.sursa.com${path}?limit=200`;
  while (next) {
    const response = await fetch(next, { headers: { Authorization: `Bearer ${process.env.SURSA_API_KEY}` } });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    yield* await response.json();
    const cursor = response.headers.get("X-Next-Cursor");
    next = cursor ? `https://api.sursa.com${path}?cursor=${encodeURIComponent(cursor)}` : null;
  }
}

The event feed

The event feed GET /v1/webhook-events is a log, so it pages a little differently:

  • Events are in ascending order, oldest first.
  • next_cursor is returned on every page, even when has_more is false. Store it, and ask again later with the same cursor to get the events that happened since. The feed is a queue you can read at your own pace.
  • An event appears in the feed within about a second of the change it describes, and never before an event that was already read.
  • The feed holds the events of the whole organization, so reading it needs a key with the admin role and the webhooks:manage scope.

Export to a data warehouse shows a complete polling loop.

Rules for cursors

  • A cursor is an opaque string of up to 512 characters. Do not parse it or build it yourself: its format can change.
  • A cursor belongs to one list and one set of filters. A cursor that is damaged, comes from another list, or is sent with different filters returns 400 with the code api.cursor_invalid.
  • Each list has a fixed sort order that ends with a unique key, so every item appears on exactly one page.
  • Lists added with cursors do not return a total count; lists that had a total before keep it. Count items as you go if you need a total.

Offset pagination

Some lists accepted offset before cursors were introduced, and still do, so that existing integrations keep working. Prefer cursor in new code. offset and cursor cannot be combined: sending both returns 422 with the code validation.failed.