Pagination
Every list endpoint returns the same envelope and pages with cursors. There are no page numbers.
{
"object": "list",
"data": [{ "object": "chatbot", "id": 12 }],
"has_more": true,
"next_cursor": "eyJpZCI6MTIsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
"previous_cursor": null,
"url": "/v1/chatbots"
}
Why cursors and not `?page=2`
Offset pagination is unstable while data is changing underneath it. If a new chatbot is created while you are on page 1, everything shifts down and page 2 repeats a row you already saw. Delete one and a row is skipped entirely.
A cursor points at a position in the ordering, not a count of rows before it. Inserts and deletes elsewhere in the list cannot shift it, so a long export neither duplicates nor skips.
Walking forwards
Pass the next_cursor you were given as starting_after:
# First page
curl "https://api.agentency.com/v1/chatbots?limit=20" \
-H "Authorization: Bearer <YOUR_KEY>"
# Next page
curl "https://api.agentency.com/v1/chatbots?limit=20&starting_after=eyJpZCI6MTIs…" \
-H "Authorization: Bearer <YOUR_KEY>"Stop when has_more is false.
# Walk every page, printing ids.
CURSOR=""
while :; do
URL="https://api.agentency.com/v1/chatbots?limit=100"
[ -n "$CURSOR" ] && URL="$URL&starting_after=$CURSOR"
BODY=$(curl -s "$URL" -H "Authorization: Bearer <YOUR_KEY>")
echo "$BODY" | jq -r '.data[].id'
[ "$(echo "$BODY" | jq -r '.has_more')" = "true" ] || break
CURSOR=$(echo "$BODY" | jq -r '.next_cursor')
doneWalking backwards
Pass previous_cursor as ending_before to step back a page. That is what
powers a "previous" button.
curl "https://api.agentency.com/v1/chatbots?limit=20&ending_before=eyJpZCI6MzIs…" \
-H "Authorization: Bearer <YOUR_KEY>"Send one or the other, never both.
Page size
limit defaults to 20 and is clamped by the server, so asking for 10,000 gets
you the maximum rather than an error.
For a bulk read, use the largest limit the endpoint allows. One request for 100 rows costs a hundredth of the rate budget that a hundred requests for one row would.
Filtering by date
List endpoints accept a created range, in Unix seconds or ISO-8601:
curl "https://api.agentency.com/v1/conversations?created[gte]=2026-01-01&created[lte]=2026-01-31&limit=100" \
-H "Authorization: Bearer <YOUR_KEY>"created[gte]— at or after.created[lte]— at or before.
Combine with cursors to export a month without holding the whole thing in memory.
Sorting
sort accepts id or created_at, newest first by default. Prefix with +
for ascending:
curl "https://api.agentency.com/v1/chatbots?sort=%2Bcreated_at" \
-H "Authorization: Bearer <YOUR_KEY>"An unrecognised sort field falls back to id rather than erroring.
A cursor encodes a position in a specific ordering. Changing `sort` between pages makes the cursor meaningless. Finish the walk, then re-sort.
Common mistakes
- Looking for
pageoroffset. Neither exists. - Building your own cursor. They are opaque; treat them as strings and pass them back unmodified.
- Ignoring
has_more. An emptydataarray withhas_more: trueis possible when a filter excludes a whole page. - Fetching one row at a time. The fastest way to hit a rate limit.
What's next
- Rate limits — budgeting a large read.
- Errors — what a bad cursor returns.
- Conversations — the endpoint most often exported.