Pagination
Every list is a cursor-paged { items, nextCursor }. How to walk one, and why there is no offset.
Every endpoint returns the same envelope:
{
"items": [],
"nextCursor": "eyJrIjoiZXZlbnRzIiwidCI6IjIwMjYtMDkt..."
}nextCursor is null on the last page and a string otherwise. That is the
only signal you need: keep going while it is not null.
Walking a list
cursor=""
while :; do
page=$(curl -sG -H "Authorization: Bearer $SUBCORE_API_KEY" \
--data-urlencode "limit=100" \
${cursor:+--data-urlencode "cursor=$cursor"} \
"https://api.subcore.ai/telemetry/events")
echo "$page" | jq -c '.items[]'
cursor=$(echo "$page" | jq -r '.nextCursor // empty')
[ -z "$cursor" ] || continue
break
doneasync function* events(params = {}) {
let cursor;
do {
const query = new URLSearchParams({ ...params, limit: "100" });
if (cursor) query.set("cursor", cursor);
const res = await fetch(
`https://api.subcore.ai/telemetry/events?${query}`,
{ headers: { Authorization: `Bearer ${process.env.SUBCORE_API_KEY}` } },
);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const page = await res.json();
yield* page.items;
cursor = page.nextCursor;
} while (cursor);
}limit
Default 25, maximum 100, minimum 1.
Asking for more than 100 is not an error — you are quietly given 100. Asking for something that is not a positive integer is an error:
{ "error": "\"limit\" must be a positive integer", "code": "VALIDATION_ERROR", "timestamp": 1788566031707 }Always read the page size from items.length rather than assuming you got what
you asked for.
Why cursors and not offsets
The cursor is a keyset cursor: it encodes the sort position of the last row you
were given — (created_at, id) for events, (last_at, sid) for sessions — and
the next page resumes strictly after it.
That matters because these tables are written to constantly. With offset=100,
events arriving between two requests shift every row down and you re-read rows
you have already seen, or skip rows you have not. A keyset cursor is immune:
"everything after this exact row" means the same thing however much has been
inserted since.
Treat the cursor as opaque. It is base64url over a small JSON object, but it is validated on the way back in: a cursor from a session list handed to an event query is refused rather than applied to the wrong columns, and a cursor whose contents were edited is refused too.
{ "error": "Cursor does not belong to this query", "code": "INVALID_CURSOR", "timestamp": 1788566031707 }Pass back exactly the string you were given, to the same endpoint that gave it to you.
Ordering
| Endpoint | Order |
|---|---|
GET /telemetry/sessions | Most recently active first (last_at descending) |
GET /telemetry/sessions/{sid} | Oldest first — the conversation in order |
GET /telemetry/events | Newest first |
GET /telemetry/runs/{runId} | Oldest first — the run in order |
The two "read one thing end to end" endpoints go forwards because you want to read them as a transcript. The two "what has been happening" endpoints go backwards because you want the newest first.