Pagination
Every collection in the Kaily API is paginated with a cursor. You ask for a page size and get an opaque cursor back; you hand that cursor to the next request. There are no page numbers.
Parameters
| Parameter | Type | Default | Notes |
|---|---|---|---|
limit | integer | 30 | Maximum 100. A value above the maximum is clamped rather than rejected; a value that is not a positive integer falls back to the default |
next | string | — | The cursor from the previous response's meta.next. Opaque: do not parse, construct or store it long-term |
The envelope
A collection response always has this shape:
{
"data": [
{ "id": "thr-6Qm2Vd", "display_id": "1042", "subject": "Order 10432 arrived damaged", "status": "open" },
{ "id": "thr-3Kd8Rn", "display_id": "1041", "subject": "Where is my refund?", "status": "resolved" }
],
"meta": {
"limit": 30,
"next": "eyJjcmVhdGVkQXQiOiIyMDI2LTA4LTI2VDA5OjEyOjQ0LjAwMFoiLCJpZCI6ImFnZW50XzAxIn0",
"has_more": true
}
}
data is the page. meta.limit is the page size that was actually applied. meta.next is the cursor for the following page, and meta.has_more tells you whether there is one. When has_more is false, next is null and you are done.
A single resource is wrapped in the same data key, without meta:
{ "data": { "id": "thr-6Qm2Vd", "display_id": "1042", "subject": "Order 10432 arrived damaged" } }
Some small configuration collections are short by nature and return the whole set in one response. They use the same envelope, with has_more: false and next: null.
There is deliberately no total
You will not find total or total_pages in meta, and this is a decision rather than an omission. Producing a total means running a second counting query over the same filters on every single page request, which is the most expensive part of a listing and is almost always thrown away by the caller.
Cursors also behave better than page numbers under concurrent writes. Offset pagination re-scans and re-orders rows on each deep page, so a record updated while you are paging can appear twice or not at all. A cursor is anchored to a position in the ordering, so a walk is stable.
If you genuinely need a number, ask for one:
curl "https://api.kaily.ai/v1/ai-agents" \
-H "Authorization: Bearer kly_live_a1b2c3d4e5f6_s3cr3tvalue"
Count endpoints arrive alongside the resources that need them. Where one exists it takes the same filters as its listing, so the count always matches the page you are about to walk.
Walking every page
Request the first page with no cursor, then follow meta.next until has_more is false.
# First page
curl "https://api.kaily.ai/v1/ai-agents?limit=100" \
-H "Authorization: Bearer kly_live_a1b2c3d4e5f6_s3cr3tvalue"
# Next page
curl "https://api.kaily.ai/v1/ai-agents?limit=100&next=eyJjcmVhdGVkQXQiOiIyMDI2LTA4LTI2VDA5OjEyOjQ0LjAwMFoiLCJpZCI6ImFnZW50XzAxIn0" \
-H "Authorization: Bearer kly_live_a1b2c3d4e5f6_s3cr3tvalue"
In Node.js:
const KAILY_KEY = process.env.KAILY_API_KEY;
async function* paginate(path, params = {}) {
let next = null;
do {
const query = new URLSearchParams({ limit: '100', ...params });
if (next) query.set('next', next);
const response = await fetch(`https://api.kaily.ai/v1${path}?${query}`, {
headers: { Authorization: `Bearer ${KAILY_KEY}` }
});
if (!response.ok) {
const body = await response.json();
throw new Error(`${response.status} ${body.error.code}: ${body.error.message}`);
}
const page = await response.json();
yield* page.data;
next = page.meta.has_more ? page.meta.next : null;
} while (next);
}
// Walk every AI agent
for await (const agent of paginate('/ai-agents')) {
console.log(agent.id, agent.name);
}
And in Python:
import os
import requests
BASE = "https://api.kaily.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['KAILY_API_KEY']}"}
def paginate(path, **params):
params = {"limit": 100, **params}
while True:
response = requests.get(f"{BASE}{path}", headers=HEADERS, params=params)
response.raise_for_status()
page = response.json()
yield from page["data"]
if not page["meta"]["has_more"]:
return
params["next"] = page["meta"]["next"]
for agent in paginate("/ai-agents"):
print(agent["id"], agent["name"])
Incremental sync
For a job that runs repeatedly, do not page through everything each time. Listings that support it accept updated_since, so you can walk only what has changed:
curl "https://api.kaily.ai/v1/ai-agents?updated_since=2026-08-25T00:00:00Z&limit=100" \
-H "Authorization: Bearer kly_live_a1b2c3d4e5f6_s3cr3tvalue"
Store the timestamp of the run, not the cursor. Cursors are for finishing a walk you have started; updated_since is for starting the next one. For near-real-time updates, subscribe to the API reference instead of polling.
Rules of thumb
- Treat
nextas opaque. It is a base64 value whose contents are an implementation detail and may change. - Do not mix filters mid-walk. A cursor is only meaningful for the same query it came from.
- Ask for
limit=100when you are backfilling — fewer, larger pages cost you less rate-limit budget. - Stop on
has_more: false, not on an emptydataarray.