Skip to main content

Pagination

Large collections — boards, cards, and comments — are cursor-paginated. A page response wraps the rows in data and the paging state in page:

{
"data": [],
"page": {
"next_cursor": null,
"has_more": false
}
}
  • has_more: true means there is a next page; fetch it by repeating the same request with cursor=<next_cursor>.
  • has_more: false (with next_cursor: null) means you have everything.

Two query parameters control paging:

ParameterMeaning
limitPage size, 1200, default 50. Anything else is 400 invalid_request.
cursorThe next_cursor from the previous page.

Cursors are opaque — never build one

A cursor is a signed, self-describing token bound to the exact query that produced it: the route, your workspace, the filters, and the page size. Treat it as an opaque string:

  • Never parse, modify, or construct a cursor. Any tampered or hand-built value fails signature verification and returns 400 invalid_request.
  • A cursor is only valid for the same query. Reusing it with different filters, another limit, or a different endpoint returns 400 invalid_request. When you change filters, start over without a cursor.
  • Cursors expire after about 24 hours. An expired cursor returns 410 with code cursor_expired — restart the listing from the first page.

Draining a collection

cURL (page manually — repeat until has_more is false):

# First page.
curl -sS "$KANBAN_API_BASE_URL/boards?limit=50" \
-H "Authorization: Bearer $KANBAN_API_TOKEN"

# Follow-up page: paste the next_cursor value from the previous response.
NEXT_CURSOR="<next_cursor-from-previous-response>"
curl -sS -G "$KANBAN_API_BASE_URL/boards" \
-H "Authorization: Bearer $KANBAN_API_TOKEN" \
--data-urlencode "limit=50" \
--data-urlencode "cursor=$NEXT_CURSOR"

JavaScript:

const baseUrl =
process.env.KANBAN_API_BASE_URL ?? "https://app.filetag.ai/api/kanban/v1";
const token = process.env.KANBAN_API_TOKEN;

async function* listAllBoards() {
let cursor = null;
do {
const query = new URLSearchParams({ limit: "50" });
if (cursor) {
query.set("cursor", cursor);
}
const response = await fetch(`${baseUrl}/boards?${query}`, {
headers: { Authorization: `Bearer ${token}` },
});
const payload = await response.json();
if (!response.ok) {
if (payload.error.code === "cursor_expired") {
cursor = null; // Restart from the first page.
continue;
}
throw new Error(
`${payload.error.code}: ${payload.error.message} (request ${payload.error.request_id})`
);
}
yield* payload.data;
cursor = payload.page.has_more ? payload.page.next_cursor : null;
} while (cursor);
}

async function main() {
for await (const board of listAllBoards()) {
console.log(board.id, board.name);
}
}

main().catch((error) => {
console.error(error);
process.exit(1);
});

Python:

import json
import os
import urllib.error
import urllib.parse
import urllib.request

BASE_URL = os.environ.get(
"KANBAN_API_BASE_URL", "https://app.filetag.ai/api/kanban/v1"
)
TOKEN = os.environ["KANBAN_API_TOKEN"]


def list_all_boards():
cursor = None
while True:
params = {"limit": "50"}
if cursor:
params["cursor"] = cursor
request = urllib.request.Request(
f"{BASE_URL}/boards?{urllib.parse.urlencode(params)}",
headers={"Authorization": f"Bearer {TOKEN}"},
)
try:
with urllib.request.urlopen(request) as response:
payload = json.load(response)
except urllib.error.HTTPError as exc:
error = json.load(exc)["error"]
if error["code"] == "cursor_expired":
cursor = None # Restart from the first page.
continue
raise RuntimeError(
f"{error['code']}: {error['message']} "
f"(request {error['request_id']})"
) from exc
yield from payload["data"]
if not payload["page"]["has_more"]:
return
cursor = payload["page"]["next_cursor"]


for board in list_all_boards():
print(board["id"], board["name"])

Ordering is stable and documented

Each collection has a fixed ordering tuple (for example, active boards are ordered by name then ID; comments oldest-first by creation time then ID), so paging is deterministic: no duplicated or skipped rows from ordering ambiguity. The exact order for each endpoint is stated in its reference page.

Note that shorter collections — columns, labels, members, subtasks, attachments — return their full data array without a page object; only the endpoints that accept cursor/limit paginate.