Subcore AI Docs
Telemetry read API

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
done
async 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

EndpointOrder
GET /telemetry/sessionsMost recently active first (last_at descending)
GET /telemetry/sessions/{sid}Oldest first — the conversation in order
GET /telemetry/eventsNewest 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.

On this page