> ## Documentation Index
> Fetch the complete documentation index at: https://dev.tryno.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Paginação

> Como percorrer listas em /api/v1: page, pageSize, o envelope com hasMore e um laço completo de varredura.

## Parâmetros

Toda lista em `/v1` aceita os mesmos dois parâmetros de query:

<ParamField query="page" type="integer" default="1">
  Página, começando em **1** (não em 0). Valores menores que 1, negativos ou não
  numéricos são **corrigidos silenciosamente para 1** — não há `400`.
</ParamField>

<ParamField query="pageSize" type="integer" default="25">
  Itens por página. Mínimo `1`, máximo `100`. Valores fora da faixa são
  **grampeados**, não rejeitados: `pageSize=5000` devolve 100, `pageSize=0`
  devolve 1.
</ParamField>

<Warning>
  Como os valores inválidos são corrigidos em silêncio, um bug no seu código
  (um `page` `NaN`, por exemplo) não estoura erro — ele simplesmente relê a
  página 1 para sempre. Se a sua varredura nunca termina, cheque primeiro se o
  `page` que você envia é realmente um número.
</Warning>

Quais endpoints paginam:

| Endpoint                     | Pagina? | Ordenação                                                              |
| ---------------------------- | ------- | ---------------------------------------------------------------------- |
| `GET /v1/members`            | Sim     | `joinedAt` desc (entradas mais recentes primeiro)                      |
| `GET /v1/posts`              | Sim     | data de publicação desc (`publishedAt`, ou `createdAt` quando ausente) |
| `GET /v1/events`             | Sim     | `startsAt` desc                                                        |
| `GET /v1/orders`             | Sim     | `createdAt` desc                                                       |
| `GET /v1/members/{username}` | Não     | item único                                                             |
| `GET /v1/me`                 | Não     | objeto único                                                           |

## Envelope de resposta

Todas as listas respondem com a mesma forma:

```json theme={"system"}
{
  "object": "list",
  "data": [{ "object": "member", "…": "…" }],
  "page": 1,
  "pageSize": 25,
  "hasMore": true
}
```

<ResponseField name="object" type="string">
  Sempre `"list"` numa resposta de lista.
</ResponseField>

<ResponseField name="data" type="object[]">
  Os itens da página. Cada item traz o próprio campo `object`: `member`, `post`,
  `event` ou `order`. Lista vazia é `[]`, nunca `null` — e **não** é erro.
</ResponseField>

<ResponseField name="page" type="integer">
  A página efetivamente servida, **já grampeada**. Compare com o que você pediu
  para detectar um parâmetro inválido.
</ResponseField>

<ResponseField name="pageSize" type="integer">
  O tamanho efetivamente aplicado, já grampeado ao teto de 100.
</ResponseField>

<ResponseField name="hasMore" type="boolean">
  `true` quando a página voltou **cheia** (`data.length === pageSize`). Veja a
  ressalva abaixo.
</ResponseField>

Respostas de item único, como `GET /v1/members/{username}`, são **planas** — não
vêm embrulhadas em `data`, e trazem `object: "member"` na raiz.

<Note>
  **`hasMore` é uma heurística, não uma contagem.** Ele é derivado do tamanho da
  página retornada. Quando o total é múltiplo exato do `pageSize` — 50 membros
  com `pageSize=25` — a última página cheia devolve `hasMore: true`, e a página
  seguinte volta vazia com `hasMore: false`. Isso custa **uma requisição extra**
  por varredura e é comportamento esperado; seu laço precisa lidar com uma página
  vazia no fim.
</Note>

## Como percorrer

<Steps>
  <Step title="Comece em page=1 com pageSize=100">
    O teto de 100 é sempre a escolha certa numa varredura: são 4× menos
    requisições que o padrão 25, e o teto de taxa é por requisição, não por item.
  </Step>

  <Step title="Processe data">
    A ordem é estável e definida pelo endpoint (tabela acima).
  </Step>

  <Step title="Pare quando hasMore for false OU data vier vazia">
    As duas condições — a segunda cobre o caso do múltiplo exato.
  </Step>

  <Step title="Imponha um teto de páginas">
    Um limite rígido evita que um bug transforme a varredura num laço infinito
    que consome o orçamento de requisições da chave.
  </Step>
</Steps>

```bash Uma página theme={"system"}
curl "https://tryno.io/api/v1/members?page=2&pageSize=100" \
  -H "Authorization: Bearer $TRYNO_API_KEY"
```

### Varredura completa

<CodeGroup>
  ```js Node.js / TypeScript theme={"system"}
  const MAX_PAGES = 1_000; // trava de segurança contra laço infinito

  /**
   * Percorre todas as páginas de um recurso de lista de /v1.
   * `trynoFetch` é o cliente com retentativa do guia de erros.
   */
  export async function* paginate(resource, { apiKey, pageSize = 100 } = {}) {
    let page = 1;

    while (page <= MAX_PAGES) {
      const query = new URLSearchParams({ page: String(page), pageSize: String(pageSize) });
      const body = await trynoFetch(`${resource}?${query}`, { apiKey });

      const items = body.data ?? [];
      for (const item of items) yield item;

      // Duas condições de parada: hasMore falso OU página vazia — a segunda
      // cobre o caso em que o total é múltiplo exato do pageSize.
      if (!body.hasMore || items.length === 0) return;
      page += 1;
    }

    throw new Error(`paginate(${resource}): teto de ${MAX_PAGES} páginas atingido`);
  }

  // Uso
  const membros = [];
  for await (const m of paginate("/members", { apiKey: process.env.TRYNO_API_KEY })) {
    membros.push(m);
  }
  console.log(`${membros.length} membros`);
  ```

  ```python Python theme={"system"}
  from typing import Iterator

  MAX_PAGES = 1_000  # trava de segurança contra laço infinito


  def paginate(resource: str, page_size: int = 100, api_key: str | None = None) -> Iterator[dict]:
      """Percorre todas as páginas de um recurso de lista de /v1.

      `tryno_get` é o cliente com retentativa do guia de erros.
      """
      page = 1

      while page <= MAX_PAGES:
          body = tryno_get(
              resource,
              params={"page": page, "pageSize": page_size},
              api_key=api_key,
          )

          items = body.get("data") or []
          yield from items

          # Pare no hasMore falso OU na página vazia (múltiplo exato do pageSize).
          if not body.get("hasMore") or not items:
              return
          page += 1

      raise RuntimeError(f"paginate({resource}): teto de {MAX_PAGES} páginas atingido")


  # Uso
  membros = list(paginate("/members"))
  print(f"{len(membros)} membros")
  ```

  ```bash cURL + jq theme={"system"}
  #!/usr/bin/env bash
  set -euo pipefail

  RESOURCE="${1:-/members}"
  PAGE=1

  while :; do
    BODY=$(curl -sf \
      -H "Authorization: Bearer $TRYNO_API_KEY" \
      "https://tryno.io/api/v1${RESOURCE}?page=${PAGE}&pageSize=100")

    COUNT=$(jq '.data | length' <<<"$BODY")
    jq -c '.data[]' <<<"$BODY"

    HAS_MORE=$(jq -r '.hasMore' <<<"$BODY")
    if [ "$HAS_MORE" != "true" ] || [ "$COUNT" -eq 0 ]; then
      break
    fi

    PAGE=$((PAGE + 1))
  done
  ```
</CodeGroup>

<Tip>
  A varredura acima consome **1 requisição por página**. Com `pageSize=100` e o
  teto de 120 req/min por chave, você percorre até 12.000 registros por minuto
  sem tocar no limite. Se precisar de mais, [trate o `429`](/guides/errors) —
  não aumente o `pageSize`, que já está no máximo.
</Tip>

<Note>
  **Não há total de registros.** Nenhuma resposta traz `total` ou `pageCount`, e
  `hasMore` não é derivado de uma contagem. Contar a tribo inteira a cada página
  seria caro, e o número estaria desatualizado antes de você lê-lo. Se a sua
  interface precisa de "página 7 de 42", ela precisa contar do seu lado — ou
  adotar rolagem infinita, que é o que o `hasMore` favorece.
</Note>

## A ressalva do offset

A paginação é por **offset** (`OFFSET = (page − 1) × pageSize`), calculado no
banco a cada requisição. Duas consequências que importam de verdade:

<Warning>
  **Varreduras longas podem duplicar ou pular itens.** Todas as listas são
  ordenadas por data **decrescente**, então registros novos entram no **começo**
  e empurram tudo para frente. Se a tribo receber 3 membros novos durante a sua
  varredura, 3 itens que estavam na fronteira entre as páginas 2 e 3 serão vistos
  duas vezes. Se registros saírem, itens são pulados.
</Warning>

Como conviver com isso:

* **Deduplique por id no destino.** `UPSERT` por `id` (ou por `username`, em
  membros) torna a duplicata inofensiva. É a mitigação mais simples e a que
  resolve o caso real.
* **Varra do fim para o começo** quando o objetivo for um retrato consistente:
  começar pela última página reduz o impacto de inserções, que só afetam o
  início.
* **Prefira [webhooks](/guides/webhooks) para sincronização incremental.** Varrer
  a lista em laço só para descobrir o que mudou é caro, é a maior fonte de `429`,
  e é exatamente o problema que o evento resolve.
* **Páginas profundas ficam mais caras.** Um `OFFSET` grande faz o banco
  percorrer e descartar todas as linhas anteriores. Numa tribo grande, a página
  500 responde mais devagar que a página 5 — mais uma razão para preferir eventos
  a varreduras periódicas de tudo.

<Card title="Próximo passo — erros e limites →" icon="triangle-alert" href="/guides/errors" horizontal>
  O cliente com retentativa usado nos exemplos acima, com backoff e jitter.
</Card>
