Pagination
List endpoints return a page at a time and give you a cursor to fetch the next one.
{
"data": [ ... ],
"pagination": {
"limit": 20,
"has_more": true,
"next_cursor": "eyJrIjoiMjAyNi0wOC0xMlQxMDoyNjo0MloiLCJpIjoiYl83YTNmIn0"
},
"request_id": "req_01J8Z3K9QW"
}| Field | Meaning |
|---|---|
limit | How many items this page could contain |
has_more | Whether another page exists |
next_cursor | Pass this as cursor to fetch the next page. null on the last page. |
Parameters
| Parameter | Default | Maximum |
|---|---|---|
limit | 20 | 100 |
cursor | none | — |
curl -G https://api.askylabs.com/v1/brands/b_7a3f.../prompts \
-H "Authorization: Bearer $ASKY_API_KEY" \
-d limit=100Then follow the cursor:
curl -G https://api.askylabs.com/v1/brands/b_7a3f.../prompts \
-H "Authorization: Bearer $ASKY_API_KEY" \
-d limit=100 \
-d cursor=eyJrIjoiMjAyNi0wOC0xMlQxMDoyNjo0MloiLCJpIjoiYl83YTNmIn0Iterating to the end
Loop until next_cursor is null. Do not stop on a short page: a page may contain fewer items than limit and still have more after it.
async function* paginate(path, key, params = {}) {
let cursor = null
do {
const url = new URL(`https://api.askylabs.com/v1${path}`)
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v)
url.searchParams.set('limit', '100')
if (cursor) url.searchParams.set('cursor', cursor)
const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } })
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const body = await res.json()
yield* body.data
cursor = body.pagination.next_cursor
} while (cursor)
}
for await (const prompt of paginate('/brands/b_7a3f.../prompts', KEY)) {
console.log(prompt.text)
}Why cursors, not offsets
A cursor points at a position in the result set rather than counting from the start. That matters because Asky data changes while you are reading it: pipelines add citations, executions, and pages continuously.
With offset pagination, a row inserted before your position shifts everything down, so offset=100 returns a row you already saw, and one you never see at all. Neither is visible as an error. Your sync just silently drops records.
A cursor is immune to that. It resumes exactly where the previous page ended, whatever has been inserted meanwhile.
Working with cursors
Treat cursors as opaque. The contents are an implementation detail. Do not decode, construct, or modify one, and do not assume the format is stable between releases.
Cursors are short-lived. Use one to continue the list you were walking. Do not store one to resume a sync tomorrow: it may be rejected with 400 invalid_cursor, and even when accepted it resumes a snapshot rather than telling you what changed. To resume a periodic sync, use a date filter instead. See filtering.
A cursor belongs to its query. Changing limit or any filter while paginating invalidates it. Keep every parameter identical for the whole walk.
Cursors are scoped to the key that issued them. A cursor from one key is not valid on another.
Result ordering
Every list endpoint has a deterministic sort, so a walk visits each item exactly once. Most lists are newest first. Where an endpoint supports an explicit sort, it is documented on that endpoint and must stay constant for the duration of a walk.
Choosing a page size
Use limit=100 for bulk reads. It is the same data in a fifth of the requests that the default would take, which matters against your rate limit.
Use a smaller page when you display results interactively and want the first page fast.
Very large result sets
If you find yourself walking hundreds of pages on every run, you are probably fetching more than you need. Narrow the date range, or sync incrementally instead of re-reading history.
If you genuinely need a full historical extract, contact us before building a paginated backfill. There is often a better way to move that much data than several hundred sequential requests.