List operations answer one page at a time, in a stable order. Each page carries opaque cursors that lead to the pages around it.
Request a page
| Parameter | Default | Meaning |
|---|---|---|
limit |
25 |
The most items in one page, from 1 to 100. Reading more takes another page. |
cursor |
None | A cursor from the previous answer’s pageInfo. Leave it out for the first page. |
direction |
forward |
Which way to traverse the order from cursor: forward or backward. |
Most lists are GET operations and take these as query parameters. Searches sent with POST take them in the JSON body instead, such as agenda.appointments.search and raiz.knowledge.search.
A few operations set their own limit bounds. agenda.appointments.list allows up to 500, and raiz.knowledge.search defaults to 8 with a maximum of 20. The operation’s reference states its bounds.
Read the page
A list’s data is a page:
| Field | Meaning |
|---|---|
object |
Always "list". |
data |
The items on this page. |
pageInfo.hasNextPage |
Whether a later page exists in the current order. |
pageInfo.hasPreviousPage |
Whether an earlier page exists in the current order. |
pageInfo.nextCursor |
The cursor for the next page, or null when there is none. |
pageInfo.previousCursor |
The cursor for the previous page, or null when there is none. |
For example, the first page of account.apiKeys.list, one key long. The call needs the account:api-keys:read scope:
curl "https://api.getarbol.com/v1/account/api-keys?limit=1" \
--header "Authorization: Bearer $ARBOL_API_KEY"{
"success": true,
"data": {
"object": "list",
"data": [
{
"id": "exampleClientId0001",
"name": "Billing backend",
"scopes": ["account:billing:read"],
"createdAt": "2026-10-01T15:00:00.000Z",
"createdBy": null,
"restrictions": null
}
],
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": false,
"nextCursor": "EXAMPLE_CURSOR_2",
"previousCursor": null
}
},
"error": null,
"metadata": { "requestId": "00000000-0000-4000-8000-000000000003" }
}Read every page
Call without a cursor, then call again with cursor set to pageInfo.nextCursor while pageInfo.hasNextPage is true. Keep every other parameter the same.
cursor=""
while :; do
params=(--data-urlencode "limit=100")
if [ -n "$cursor" ]; then params+=(--data-urlencode "cursor=$cursor"); fi
page=$(curl -sS --get https://api.getarbol.com/v1/account/api-keys "${params[@]}" \
--header "Authorization: Bearer $ARBOL_API_KEY")
jq -e '.success' <<<"$page" >/dev/null || { echo "$page" >&2; break; }
jq -c '.data.data[]' <<<"$page"
[ "$(jq -r '.data.pageInfo.hasNextPage' <<<"$page")" = "true" ] || break
cursor=$(jq -r '.data.pageInfo.nextCursor' <<<"$page")
doneimport { createClient } from "@arbol-ai/sdk";
const arbol = createClient({
baseUrl: "https://api.getarbol.com",
token: process.env.ARBOL_API_KEY!,
});
let cursor: string | undefined;
do {
const { data: page } = await arbol.account.apiKeys.list({ limit: 100, cursor });
for (const key of page.data) console.log(key.id, key.name);
cursor = page.pageInfo.hasNextPage ? (page.pageInfo.nextCursor ?? undefined) : undefined;
} while (cursor);Cursor rules
- Cursors are opaque. Use them exactly as the API returned them. Don’t build, edit or decode one.
- A cursor belongs to its query. Send it with the same filters that produced it. Raíz refuses a cursor from another list with
400 INVALID_CURSOR. - Some reads don’t page. A few take only
limitand answer nopageInfo, such asagenda.firstAvailability.get. Their reference shows it.