Paginação
Como paginar as listas da API da Axuz com cursores keyset (created_at e id), os quatro formatos que existem hoje, quais rotas paginam e loops prontos em Node e Python.
As listas grandes da API usam paginação keyset: em vez de “pule N itens” (OFFSET), você diz “continue depois deste item”. O ponto de partida é quase sempre o par (created_at, id) do último item da página. Isso evita perder ou repetir itens quando algo é criado enquanto você pagina, e o custo no servidor não cresce com a profundidade.
Regras que valem para todos os formatos
- O cursor é opaco. Pegue o valor devolvido e mande de volta tal e qual. Não monte, não edite, não interprete: o conteúdo interno (hoje, algo como
timestamp|uuid, em alguns casos codificado em base64) pode mudar. - Codifique na URL. Cursores contêm
|,:e+. UseURLSearchParams(Node) ouurllib.parse.urlencode(Python); nunca concatene na mão. - Fim da lista: pare quando a página vier vazia ou quando o indicador de “próxima página” estiver ausente,
nullou vazio. Cada formato abaixo diz qual. limité um pedido, não uma garantia. Cada rota tem um padrão e um teto. Em várias rotas um valor fora da faixa não é limitado ao teto: cai no padrão, sem erro. Releia o tamanho da página devolvida em vez de assumir o que pediu.- Cursor inválido tem comportamento diferente por serviço: algumas rotas respondem
400(cursor inválido), outras ignoram o cursor e devolvem a primeira página. Esse segundo caso, num loop sem proteção, vira loop infinito (veja o código abaixo). - Lista nula: o servidor normaliza listas vazias para
[], mas trateitems: null(ou ausente) como lista vazia no cliente. É barato e protege contra rotas e serviços que ainda não normalizam.
Formato A: cursor e next_cursor (string)
O formato mais comum nas rotas novas. Parâmetros: limit e cursor. Resposta: {"items": [...], "next_cursor": "..."}.
Rotas que usam esse formato:
- GET
/me/calendar/upcoming(serviço de calendário) - GET
/teams/{teamId}/forms/{formId}/submissionseGET /teams/{teamId}/submissions(serviço de formulários) - GET
/teams/{teamId}/tasks/checklist-items/search, que também recebeq(mínimo 2 caracteres; com menos, devolveitems: []sem consultar o banco)
next_cursor é uma string quando há mais páginas e fica vazio, null ou ausente na última. Na busca de checklist ele é null no fim; em outras rotas, "". Teste “falsy”, não um valor específico.
Node
// node 20+, ESM. Percorre qualquer rota do formato A.
const API = process.env.AXUZ_API ?? 'https://api-forms.axuz.net';
export async function* paginate(path, params = {}, { limit = 50, maxPages = 10_000 } = {}) {
let cursor = '';
const seen = new Set();
for (let page = 0; page < maxPages; page++) {
const qs = new URLSearchParams({ ...params, limit: String(limit) });
if (cursor) qs.set('cursor', cursor);
const res = await fetch(`${API}${path}?${qs}`, {
headers: { Authorization: `Bearer ${process.env.AXUZ_TOKEN}` },
});
if (!res.ok) throw new Error(`HTTP ${res.status} em ${path}`);
const body = await res.json();
const items = body.items ?? []; // null ou ausente = vazio
yield* items;
const next = body.next_cursor;
if (!next || items.length === 0) return; // fim da lista
if (seen.has(next)) throw new Error('cursor repetido: o servidor ignorou o cursor?');
seen.add(next);
cursor = next;
}
throw new Error('maxPages excedido');
}
for await (const item of paginate('/me/calendar/upcoming', {}, { limit: 100 })) {
console.log(item.id);
}
Python
import json
import os
import urllib.parse
import urllib.request
API = os.environ.get("AXUZ_API", "https://api-calendar.axuz.net")
def paginate(path, params=None, limit=50, max_pages=10_000):
"""Gera todos os itens de uma rota do formato A."""
cursor = ""
seen = set()
for _ in range(max_pages):
query = dict(params or {}, limit=limit)
if cursor:
query["cursor"] = cursor
req = urllib.request.Request(
f"{API}{path}?{urllib.parse.urlencode(query)}",
headers={"Authorization": "Bearer " + os.environ["AXUZ_TOKEN"]},
)
with urllib.request.urlopen(req, timeout=30) as res:
body = json.load(res)
items = body.get("items") or [] # None ou ausente = vazio
yield from items
nxt = body.get("next_cursor")
if not nxt or not items:
return # fim da lista
if nxt in seen:
raise RuntimeError("cursor repetido: o servidor ignorou o cursor?")
seen.add(nxt)
cursor = nxt
raise RuntimeError("max_pages excedido")
for item in paginate("/me/calendar/upcoming", limit=100):
print(item["id"])
Formato B: after_ts e after_id, com next_cursor objeto
Usado pelas listagens de arquivos: GET/teams/{teamId}/files, GET /teams/{teamId}/drive/files e GET /me/files.
Parâmetros: limit (1 a 500), after_ts (RFC 3339, com nanossegundos) e after_id. A resposta traz um objeto em next_cursor, com os dois valores a reenviar:
{
"items": [ { "id": "…", "created_at": "2026-10-22T12:00:00.123456789Z" } ],
"next_cursor": { "after_ts": "2026-10-22T12:00:00.123456789Z", "after_id": "…" }
}
Três armadilhas desse formato, todas confirmadas no código:
- Sem
limitválido não há paginação. Se você omitirlimit, ou mandar um valor fora de 1 a 500, a rota devolve todos os arquivos de uma vez (comportamento de compatibilidade), semnext_cursor. next_cursorsó aparece quando a página veio cheia (items.length == limit). Se o total for um múltiplo exato delimit, a última página cheia ainda traznext_cursor, e a requisição seguinte volta vazia. Isso é normal: pare quando a página vier vazia.- Mande
after_tseafter_idjuntos, vindos do mesmonext_cursor.
let after = null;
for (;;) {
const qs = new URLSearchParams({ limit: '200' });
if (after) { qs.set('after_ts', after.after_ts); qs.set('after_id', after.after_id); }
const res = await fetch(`https://files-api.axuz.net/teams/${teamId}/files?${qs}`, { headers });
const body = await res.json();
const items = body.items ?? [];
for (const f of items) console.log(f.id);
if (!body.next_cursor || items.length === 0) break;
after = body.next_cursor;
}
Formato C: before, sem cursor devolvido
Algumas listas ordenadas do mais novo para o mais antigo usam o timestamp do último item como cursor, e não devolvem next_cursor. Quem calcula o próximo before é o cliente:
| Rota | Parâmetros | before compara com |
|---|---|---|
GET/teams/{teamId}/timelines/{timelineId}/posts | limit (padrão 50), before (RFC 3339) | created_at do post |
GET/teams/{teamId}/feeds/{feedId}/posts | limit (padrão 50), before (RFC 3339), include_drafts | published_at, ou created_at se nulo |
GET/me/notes | limit (padrão 50, máx. 200), before (RFC 3339) | updated_at da nota |
Resposta: apenas {"items": [...]}. Para a próxima página, envie before igual ao timestamp do último item recebido. Pare quando vier menos itens que o limit (ou nenhum).
Cuidados: before é comparação estrita, então itens com exatamente o mesmo timestamp na borda da página podem ser pulados; peça páginas maiores se isso importar. Em /me/notes, as notas fixadas e a ordem manual vêm antes da ordenação por updated_at, e before filtra só a parte não fixada; para a lista completa e estável, use limit=200. Um before que não é data válida dá 400 invalid before nas rotas de equipe, e em /me/notes é ignorado (devolve a primeira página).
Formato D: after_name e after_id (membros)
GET/teams/{teamId}/members pagina por (name, user_id): limit (padrão 100), after_name, after_id. A resposta tem next_cursor como objeto com after_name e after_id; na última página o objeto vem sem campos ({}). Pare quando next_cursor.after_id não existir.
Auditoria: cursor ou offset
GET/teams/{teamId}/admin/audit aceita cursor (keyset, o caminho recomendado) ou offset (apenas para saltos curtos; é a única rota documentada aqui que faz OFFSET). next_cursor só aparece com a página cheia.
O que não pagina
Muitas listas devolvem tudo de uma vez, com ou sem teto fixo, e não têm parâmetros de página. Alguns exemplos: GET/me/api-tokens (até 200, mais recentes primeiro, sem cursor), GET /me/summary (até 200 equipes) e as listas de apoio (colunas de um quadro, papéis, etiquetas). Se a rota não lista limit nem cursor na referência, assuma que ela não pagina e que o tamanho da resposta é limitado pelo servidor. Na dúvida, leia a descrição da operação: este ainda é um ponto em que a especificação gerada nem sempre declara todos os parâmetros como parâmetros formais (por exemplo, before só aparece no texto da descrição de algumas rotas acima).