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: truemeans there is a next page; fetch it by repeating the same request withcursor=<next_cursor>.has_more: false(withnext_cursor: null) means you have everything.
Two query parameters control paging:
| Parameter | Meaning |
|---|---|
limit | Page size, 1–200, default 50. Anything else is 400 invalid_request. |
cursor | The 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 returns400 invalid_request. When you change filters, start over without a cursor. - Cursors expire after about 24 hours. An expired cursor returns
410with codecursor_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.