Skip to content
Beta

Guides Lists and pagination

Lists and pagination

How list endpoints return records a page at a time, how to read every page, and what order and filters a list supports.

List endpoints, such as GET /v1/leads or GET /v1/calls, return a team's records one page at a time. This page explains how to read every page, how a list is ordered and filtered, and which endpoints are not paged at all.

Pages and cursors

Every page comes in the same list envelope:

JSON
{
  "object": "list",
  "data": [
    {
      "id": "call_m2kUbFcayHlZ5Lgs9xZNmDS3c3Jvk",
      "object": "call",
      "livemode": false,
      "team": "team_r9uYrIW4gSFJJ6hZQ9ip1UIpPaAD1CF7NR",
      "lead": "lead_r72EfbRNUzuqORFJR0O8HxAwEXd93jGUTYMaMMXKSF0QbLS8CiUKJpEP34LFQG",
      "direction": "inbound",
      "started_at": "2026-09-28T10:17:00.000Z",
      "duration_seconds": 184,
      "outcome": "answered_ai",
      "outcome_basis": "inbound_verdict",
      "version": 2,
      "updated_at": "2026-09-28T10:21:04.010Z"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJ2IjoxLCJrIjoiY2FsbCIsImEiOiJjYWxsX20ya1ViRmNheUhsWjVMZ3M5eFpObURTM2MzSnZrIiwicyI6ImM0OTk4YTdlNDJkYiJ9357H5Y"
}
  • limit sets the page size, from 1 to 100. The default is 25. You can change it from one page to the next.
  • While has_more is true, send next_cursor back as cursor to get the next page. On the last page, has_more is false and next_cursor is null.
  • A page can hold fewer items than limit, or none at all, and still say has_more: true. Totem skips records it has not prepared yet, and ends a page early rather than make you wait. Keep going until has_more is false.

A cursor is opaque: store it exactly as you received it, and never build or edit one. It works only with the endpoint, the filters and the team it came from. Any other use gets 400 invalid_request, with param set to cursor. A conversations cursor also stops working if the conversation it points at is erased: if a cursor you did not change gets 400 invalid_request with param set to cursor, start that list again from the first page.

Read every page

To read a whole list, keep the same filters and the same Totem-Team on every request, and stop only when has_more is false. These loops list a team's calls with cURL and jq, with TypeScript on Node.js 22.18 or later (an ES module, so await works at the top level), and with Python and requests:

list-calls.shBash
cursor=""
while :; do
  page=$(curl -sS --fail-with-body \
    "https://api.totemstudio.ai/v1/calls?limit=100${cursor:+&cursor=$cursor}" \
    -H "Authorization: Bearer $TOTEM_API_KEY" \
    -H "Totem-Team: $TOTEM_TEAM_ID") || { printf '%s\n' "$page" >&2; break; }
  printf '%s\n' "$page" | jq -r '.data[].id'
  [ "$(printf '%s' "$page" | jq -r '.has_more')" = true ] || break
  next=$(printf '%s' "$page" | jq -er '.next_cursor') || { echo 'The page has no next_cursor' >&2; break; }
  [ "$next" != "$cursor" ] || { echo 'The list repeated a cursor' >&2; break; }
  cursor=$next
done
list-calls.tsTypeScript
interface CallPage {
  data: Array<{ id: string }>
  has_more: boolean
  next_cursor: string | null
}

const apiKey = process.env.TOTEM_API_KEY
if (!apiKey) throw new Error('Set TOTEM_API_KEY on your server')
const headers: Record<string, string> = { Authorization: `Bearer ${apiKey}` }
if (process.env.TOTEM_TEAM_ID) headers['Totem-Team'] = process.env.TOTEM_TEAM_ID

let cursor: string | null = null
while (true) {
  const url = new URL('https://api.totemstudio.ai/v1/calls')
  url.searchParams.set('limit', '100')
  if (cursor) url.searchParams.set('cursor', cursor)
  const response = await fetch(url, { headers, signal: AbortSignal.timeout(30_000) })
  if (!response.ok) {
    const problem = (await response.json().catch(() => ({}))) as { code?: string }
    throw new Error(`Totem API ${response.status} ${problem.code ?? response.statusText}, request ${response.headers.get('Totem-Request-Id') ?? 'n/a'}`)
  }
  const page = (await response.json()) as CallPage
  // Your code goes here. Print ids, never whole records: they hold personal data.
  for (const call of page.data) console.log(call.id)
  if (!page.has_more) break
  if (!page.next_cursor || page.next_cursor === cursor) throw new Error('The list did not move forward')
  cursor = page.next_cursor
}
list_calls.pyPython
# pip install requests
import os

import requests

headers = {"Authorization": f"Bearer {os.environ['TOTEM_API_KEY']}"}
if os.environ.get("TOTEM_TEAM_ID"):
    headers["Totem-Team"] = os.environ["TOTEM_TEAM_ID"]

params = {"limit": 100}
while True:
    response = requests.get("https://api.totemstudio.ai/v1/calls", headers=headers, params=params, timeout=30)
    if not response.ok:
        try:
            code = response.json().get("code")
        except ValueError:
            code = None
        raise RuntimeError(
            f"Totem API {response.status_code} {code or response.reason}, "
            f"request {response.headers.get('Totem-Request-Id', 'n/a')}"
        )
    page = response.json()
    # Your code goes here. Print ids, never whole records: they hold personal data.
    for call in page["data"]:
        print(call["id"])
    if not page["has_more"]:
        break
    if not page["next_cursor"] or page["next_cursor"] == params.get("cursor"):
        raise RuntimeError("The list did not move forward")
    params["cursor"] = page["next_cursor"]

Each loop also stops when the API answers an error, or when a page repeats the cursor it was asked for, rather than asking for the same page forever. To keep a copy of a team, use the changes feed instead, as Keeping a copy explains.

Order

A list comes in a stable order that means nothing: it is not by date, and not by name. If you need an order, sort on your side, for example by created_at.

Filters

GET /v1/leads is the only list with filters. It takes at most one of them, with one value:

  • board: the leads on one board (brd_…);
  • stage: the leads in one stage (stg_…);
  • status: the leads with one status, such as qualified.
cURL
curl "https://api.totemstudio.ai/v1/leads?status=qualified" \
  -H "Authorization: Bearer $TOTEM_API_KEY" \
  -H "Totem-Team: $TOTEM_TEAM_ID"

Two filters in one request get 400 invalid_request, with param naming one of them. A board or stage the team does not have returns an empty page, not an error.

There is no updated_since filter. To find out what changed, use the changes feed: Keep a copy in sync explains it.

Lists that come whole

Three lists are short, so they come whole, in one response, and take no limit or cursor:

They use the same envelope, with has_more: false and next_cursor: null. Sending limit or cursor to one of them gets 400 invalid_request.

Search is not a list

POST /v1/leads/search finds leads by exact phone or email, sent in the JSON body. It returns at most 10 leads, in one response that never pages: has_more is always false and next_cursor is always null. When many records share a phone number or an email address, search may not return them all.

Records that are not ready yet

Totem prepares each record for the API shortly after it changes. Until then, lists and search skip a new record, and a request for it by id, such as GET /v1/leads/{id}, answers 404 resource_missing. Do not retry in a loop: the changes feed delivers the record as soon as it is ready.

Keeping a copy

A list shows the records as they are now. It is the right tool to look something up, not to keep a copy: re-reading every list to find changes costs far more of your daily quota and still misses removals. To keep a copy of a team, follow the changes feed, as Keep a copy in sync explains.

API version 1.0a