# Paginação, filtros e ordenação

> Listas paginadas por cursor, com limite de 1 a 100, filtros de lista fechada e reconciliação por atualizado_desde.

URL: https://precificador3d.com.br/docs/api/paginacao

Toda lista é paginada por **cursor**. Mande `limite` (de 1 a 100, padrão 25) e, a partir da segunda página, o `cursor` que veio na anterior.

**Formato de toda lista**

```json
{
  "dados": [ … ],
  "proximo_cursor": "eyJ2IjoxfQ"
}
```

- `proximo_cursor` é `null` na última página.
- O cursor é opaco: não monte nem altere (`400 cursor_invalido`).
- Não existe `offset`: o custo cresceria a cada página.
- Página com `limite` acima de 25 conta como operação cara nos limites.

**JavaScript**

```js
let cursor = null;
const produtos = [];
do {
  const url = new URL('https://api.precificador3d.com.br/v1/produtos');
  url.searchParams.set('limite', '25');
  if (cursor) url.searchParams.set('cursor', cursor);
  const resposta = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.PC3D_CHAVE}` },
  });
  if (resposta.status === 429) {
    await new Promise((r) => setTimeout(r, Number(resposta.headers.get('Retry-After') ?? 5) * 1000));
    continue;
  }
  if (!resposta.ok) throw new Error(`Erro ${resposta.status}`);
  const pagina = await resposta.json();
  produtos.push(...pagina.dados);
  cursor = pagina.proximo_cursor;
} while (cursor);
```

**Python**

```python
import os
import time

import requests

cabecalhos = {"Authorization": f"Bearer {os.environ['PC3D_CHAVE']}"}
produtos, cursor = [], None
while True:
    params = {"limite": 25, **({"cursor": cursor} if cursor else {})}
    resposta = requests.get("https://api.precificador3d.com.br/v1/produtos", headers=cabecalhos, params=params, timeout=10)
    if resposta.status_code == 429:
        time.sleep(int(resposta.headers.get("Retry-After", "5")))
        continue
    resposta.raise_for_status()
    pagina = resposta.json()
    produtos += pagina["dados"]
    cursor = pagina["proximo_cursor"]
    if not cursor:
        break
```

## Filtros

Cada lista aceita uma lista fechada de filtros, todos indexados. Filtro fora da lista responde `400 filtro_desconhecido`. Não há busca por texto na v1.

| Lista | Filtros |
| --- | --- |
| Produtos | `status`, `categoria_id`, `sku`, `atualizado_desde` |
| Cotações | `status`, `criado_desde`, `atualizado_desde` |
| Pedidos de produção | `situacao`, `tipo`, `atualizado_desde` |
| Estoque | `produto_id`, `variacao_id`, `local_id`, `atualizado_desde` |
| Insumos | `tipo`, `abaixo_do_minimo` |

## Ordenação

`ordem=atualizado_em`, `-atualizado_em` (padrão), `criado_em` ou `-criado_em`. O `-` na frente inverte.

## Reconciliar com os webhooks

Webhook pode atrasar ou se perder no seu lado. De tempos em tempos, peça só o que mudou desde a última sincronização: `atualizado_desde=<data>&ordem=atualizado_em`.

## Cache com ETag

Toda leitura devolve `ETag`. Mande o valor em `If-None-Match` na próxima vez: se nada mudou, a resposta é `304`, sem corpo, e **não gasta a cota mensal**. É o jeito barato de consultar o catálogo de hora em hora.
