Los endpoints de lista, como GET /v1/leads o GET /v1/calls, devuelven los registros de un equipo página a página. Esta página explica cómo leer todas las páginas, cómo se ordena y se filtra una lista y qué endpoints no se paginan.
Páginas y cursores
Todas las páginas llegan en el mismo sobre de lista:
{
"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"
}limitfija el tamaño de la página, de 1 a 100. Por defecto es 25. Puedes cambiarlo de una página a otra.- Mientras
has_moreseatrue, devuelvenext_cursorcomocursorpara obtener la página siguiente. En la última página,has_moreesfalseynext_cursoresnull. - Una página puede traer menos elementos que
limit, o ninguno, y aun así indicarhas_more: true. Totem omite los registros que aún no ha preparado y prefiere terminar antes una página a hacerte esperar. Sigue pidiendo hasta quehas_moreseafalse.
Un cursor es opaco: guárdalo tal como lo recibiste y no construyas ni modifiques ninguno. Solo sirve con el endpoint, los filtros y el equipo de los que salió. Cualquier otro uso recibe 400 invalid_request, con param igual a cursor. Un cursor de conversaciones también deja de funcionar si se suprime la conversación a la que apunta: si un cursor que no cambiaste recibe 400 invalid_request con param igual a cursor, vuelve a empezar esa lista desde la primera página.
Lee todas las páginas
Para leer una lista entera, mantén los mismos filtros y el mismo Totem-Team en cada petición, y para solo cuando has_more sea false. Estos bucles listan las llamadas de un equipo con cURL y jq, con TypeScript en Node.js 22.18 o posterior (un módulo ES, así que await funciona en el nivel superior) y con Python y requests:
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 'La página no trae next_cursor' >&2; break; }
[ "$next" != "$cursor" ] || { echo 'La lista repitió un cursor' >&2; break; }
cursor=$next
doneinterface CallPage {
data: Array<{ id: string }>
has_more: boolean
next_cursor: string | null
}
const apiKey = process.env.TOTEM_API_KEY
if (!apiKey) throw new Error('Define TOTEM_API_KEY en tu servidor')
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
// Aquí va tu código. Muestra identificadores, nunca registros enteros: contienen datos personales.
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('La lista no avanzó')
cursor = page.next_cursor
}# 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()
# Aquí va tu código. Muestra identificadores, nunca registros enteros: contienen datos personales.
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("La lista no avanzó")
params["cursor"] = page["next_cursor"]Cada bucle se detiene también cuando la API responde con un error, o cuando una página repite el cursor que se le pidió, en lugar de pedir la misma página para siempre. Para mantener una copia de un equipo, usa el feed de cambios, como se explica en Mantener una copia.
Orden
Una lista llega en un orden estable que no significa nada: no es por fecha ni por nombre. Si necesitas un orden, ordena tú los datos, por ejemplo por created_at.
Filtros
GET /v1/leads es la única lista con filtros. Admite como máximo uno, con un solo valor:
board: los leads de un tablero (brd_…);stage: los leads de una etapa (stg_…);status: los leads con un estado, comoqualified.
curl "https://api.totemstudio.ai/v1/leads?status=qualified" \
-H "Authorization: Bearer $TOTEM_API_KEY" \
-H "Totem-Team: $TOTEM_TEAM_ID"Dos filtros en una misma petición reciben 400 invalid_request, con param indicando uno de ellos. Un tablero o una etapa que el equipo no tiene devuelve una página vacía, no un error.
No existe un filtro updated_since. Para saber qué ha cambiado, usa el feed de cambios, que se explica en Sincroniza una copia.
Listas que llegan completas
Tres listas son cortas, así que llegan completas, en una sola respuesta, y no admiten limit ni cursor:
GET /v1/teams, los equipos que lee tu clave;GET /v1/members, los miembros de un equipo;GET /v1/booking_services, los servicios reservables de un equipo.
Usan el mismo sobre, con has_more: false y next_cursor: null. Si envías limit o cursor a alguna de ellas, recibes 400 invalid_request.
La búsqueda no es una lista
POST /v1/leads/search busca leads por teléfono o correo electrónico exactos, enviados en el cuerpo JSON. Devuelve como máximo 10 leads, en una sola respuesta que nunca se pagina: has_more siempre es false y next_cursor siempre es null. Si muchos registros comparten un teléfono o una dirección de correo electrónico, puede que la búsqueda no los devuelva todos.
Registros que aún no están listos
Totem prepara cada registro para la API poco después de que cambie. Hasta entonces, las listas y la búsqueda omiten un registro nuevo, y una petición de ese registro por su identificador, como GET /v1/leads/{id}, responde 404 resource_missing. No reintentes en bucle: el feed de cambios entrega el registro en cuanto está listo.
Mantener una copia
Una lista muestra los registros tal como están ahora. Sirve para consultar algo, no para mantener una copia: volver a leer todas las listas para encontrar cambios consume mucho más de tu cuota diaria y aun así no detecta las eliminaciones. Para mantener una copia de un equipo, sigue el feed de cambios, como explica Sincroniza una copia.