Saltar al contenido
Beta

Guías Listas y paginación

Listas y paginación

Cómo devuelven los endpoints de lista los registros página a página, cómo leer todas las páginas y qué orden y qué filtros admite una lista.

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:

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 fija 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_more sea true, devuelve next_cursor como cursor para obtener la página siguiente. En la última página, has_more es false y next_cursor es null.
  • Una página puede traer menos elementos que limit, o ninguno, y aun así indicar has_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 que has_more sea false.

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:

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 'La página no trae next_cursor' >&2; break; }
  [ "$next" != "$cursor" ] || { echo 'La lista repitió un 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('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
}
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()
    # 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, como qualified.
cURL
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:

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.

Versión de la API 1.0a