Skip to Content
APIPlatformPagination

Pagination

Every list endpoint in the Canvas API pages the same way: a named array plus a page object holding an opaque cursor. There are no page numbers and no offsets.

{ "canvases": [ { "id": "product-launch", "handle": "canvas:9Kd2…:product-launch", "revision": "…", "nodeCount": 14, "connectionCount": 9 } ], "page": { "limit": 25, "hasMore": true, "nextCursor": "eyJ2IjoxLCJzY29wZSI6ImNhbnZhc2VzOl8…" } }
FieldTypeMeaning
page.limitintegerThe page size actually used for this response
page.hasMorebooleanWhether more items exist after this page
page.nextCursorstring or nullPass as cursor on the next request. null when hasMore is false.

The item array keeps its own resource-specific name — canvases, assets, recipes, actions, models, connections — so a generic pager needs to know which key to read, or read the only array-valued key in the object.

Per-endpoint limits

The limit ceiling is not uniform. Sending a value above the endpoint’s maximum is a 400 input_validation_error, not a silent clamp.

EndpointArray keylimit rangeDefault
GET /canvasescanvases1–2525
GET /assetsassets1–200server default
GET /recipesrecipes1–100server default
GET /actionsactions1–100server default
GET /modelsmodels1–100server default
GET /providersconnections1–100server default

Canvases cap at 25 for a concrete reason: each CanvasSummary — including nodeCount and connectionCount — is derived from the complete graph document, so a page of canvas summaries is a page of full document reads. A larger page would be a slower, less reliable request rather than a faster walk. Do not treat 25 as an oversight to work around; treat it as the endpoint telling you its real cost.

GET /campaigns is the one list that does not paginate. It is a deprecated legacy surface and returns a bare runs array with no page object.

Walking a list

Request the first page

Send limit if you want a specific page size. Omit cursor.

curl -sS "https://app.gavana.ai/api/canvas-agent/v1/canvases?limit=25" \ -H "Authorization: Bearer $GAVANA_AGENT_TOKEN"

Follow nextCursor

While page.hasMore is true, send page.nextCursor back as the cursor query parameter — with every other filter unchanged.

curl -sS --get "https://app.gavana.ai/api/canvas-agent/v1/canvases" \ --data-urlencode "limit=25" \ --data-urlencode "cursor=$NEXT_CURSOR" \ -H "Authorization: Bearer $GAVANA_AGENT_TOKEN"

Stop on hasMore false

nextCursor is null at that point. Do not send it.

async function* listAllCanvases(token: string) { let cursor: string | null = null do { const url = new URL('https://app.gavana.ai/api/canvas-agent/v1/canvases') url.searchParams.set('limit', '25') if (cursor) url.searchParams.set('cursor', cursor) const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } }) if (!response.ok) throw new Error(`${response.status} ${response.headers.get('x-request-id')}`) const body = await response.json() yield* body.canvases cursor = body.page.hasMore ? body.page.nextCursor : null } while (cursor) }

Always bound the loop in production — a page counter or a hard item cap — so a bug in cursor handling cannot turn into an unbounded request loop.

Cursor rules

A cursor is a scope-bound continuation token, at most 8192 characters. Treat it as opaque: it is not signed and carries no secret, so it authenticates nothing — the request’s own bearer token does that. What the cursor does carry is a digest of the list it came from, which is why it is rejected on a different list. Four rules govern it, and each one is enforced rather than advisory:

  1. Reuse it with the same filters. The cursor encodes the scope of the list it came from — the endpoint plus the filter values you sent. Send it back with a different canvasId, ownerUid, q, provider, or capability and you get 400 input_validation_error with the message "cursor is invalid or belongs to a different list." limit is not part of the scope, so you may change the page size mid-walk.
  2. Do not decode or construct one. It carries an internal version, the scope digest, and a position key. Its structure is not part of the contract and may change.
  3. Do not persist one long-term. A cursor is a position in a live list, not a bookmark. Storing one for hours and resuming from it is not a supported pattern — resume by re-walking from the start, or by filtering on data you control.
  4. One cursor, one next page. Cursors are not random-access. There is no way to jump to page 5, and no total count is returned by any list endpoint.

Ordering, and what changes underneath you

Canvases and assets are ordered updatedAt descending, then handle descending — newest first, with a stable tiebreak. For canvases, owned and shared records are merged into that single recent-first order.

That ordering has a direct consequence for a live list, because a cursor identifies the last item of the previous page, not a snapshot:

What happens mid-walkWhat you observe
An item is updated and jumps to the topYou may see it again on a later page if it was already returned, or miss it if it moved past your position
A new item is createdIt sorts above your cursor position and will not appear in the remainder of this walk
An item is deleted, including the one the cursor points atThe walk continues from the next older item rather than restarting or duplicating a page

None of this is a defect to work around — it is the normal behaviour of cursor paging over mutable data, and Gavana handles the worst case (a deleted cursor item) gracefully rather than erroring. Two habits keep it from surprising you:

  • Deduplicate by handle as you collect. It costs a Set and removes the entire class of “why did this canvas appear twice”.
  • Treat a full walk as approximate, not as a transactional snapshot. If you need exactness, walk to completion quickly and re-walk rather than pausing.

Pagination is not the same as scope

An empty page is not proof that nothing exists. GET /canvases returns what this token can see, and GET /assets likewise. A token without asset:read does not get a filtered asset list — it gets 403 forbidden. Check GET /auth/status before concluding that an account has no canvases.

Last updated on