# Paginação

`/v1/buscar` pagina por offset. Oportunidades e despesas federais paginam por cursor, preso à publicação consultada. Cada página é uma chamada e conta na cota.

## Offset: empresas

[`/v1/buscar`](https://fonteza.com.br/docs/referencia/buscar.md) recebe `limit` (1 a 100, padrão 20) e `offset` (padrão 0). `has_more` indica se há ao menos mais um resultado.

```bash
curl -G https://api.fonteza.com.br/v1/buscar \
  -H "X-Api-Key: $FONTEZA_KEY" \
  --data-urlencode "q=transportadora" \
  --data-urlencode "uf=SP" \
  --data-urlencode "limit=100" \
  --data-urlencode "offset=100"
```

- `total` vem `null`. Para o total exato, envie `incluir_total=true`; a contagem é mais lenta em buscas grandes.
- Páginas fundas são caras. Refine por UF, município, CNAE ou porte em vez de percorrer milhares de resultados.

## Cursor: oportunidades e despesas

[`/v1/oportunidades`](https://fonteza.com.br/docs/referencia/oportunidades.md) e [`/v1/empresas/{cnpj}/despesas-federais`](https://fonteza.com.br/docs/referencia/empresas-cnpj-despesas-federais.md) recebem `limit` (1 a 100, padrão 20) e `cursor`. Enquanto `has_more` for `true`, envie o `next_cursor` com os mesmos filtros.

- O cursor é opaco: não monte nem altere.
- Trocar filtros mantendo o cursor responde `422`.
- O cursor fica preso à publicação (`meta.geracao`). Se uma nova carga entrar no ar no meio da paginação, a resposta é `409 geracao_oportunidades_expirada` ou `409 geracao_despesas_expirada`: recomece sem cursor e descarte o que veio da publicação anterior.

Python:

```python
import os
import requests

URL = "https://api.fonteza.com.br/v1/oportunidades"
HEADERS = {"X-Api-Key": os.environ["FONTEZA_KEY"]}
FILTROS = {"q": "mobiliário", "status": "aberta", "limit": 100}


def todas_as_paginas():
    params = dict(FILTROS)
    while True:
        resposta = requests.get(URL, headers=HEADERS, params=params, timeout=30)
        if resposta.status_code == 409:
            params = dict(FILTROS)  # nova publicação: recomece do início
            continue
        resposta.raise_for_status()
        pagina = resposta.json()
        yield from pagina["items"]
        if not pagina["has_more"]:
            return
        params = {**FILTROS, "cursor": pagina["next_cursor"]}
```

JavaScript:

```javascript
const FILTROS = { q: "mobiliário", status: "aberta", limit: "100" };

async function* todasAsPaginas() {
  let params = { ...FILTROS };
  while (true) {
    const url = new URL("https://api.fonteza.com.br/v1/oportunidades");
    url.search = new URLSearchParams(params);
    const resposta = await fetch(url, { headers: { "X-Api-Key": process.env.FONTEZA_KEY } });
    if (resposta.status === 409) {
      params = { ...FILTROS }; // nova publicação: recomece do início
      continue;
    }
    if (!resposta.ok) throw new Error(`${resposta.status}: ${await resposta.text()}`);
    const pagina = await resposta.json();
    yield* pagina.items;
    if (!pagina.has_more) return;
    params = { ...FILTROS, cursor: pagina.next_cursor };
  }
}
```
