# Paginación

Cursores, orden y consistencia.

Las listas devuelven `data`, `has_more` y `next_cursor`. Pide la siguiente página pasando `cursor=<next_cursor>`, con los mismos filtros.

| Parámetro | Valor |
|---|---|
| `limit` | De 1 a 100. Por omisión, 25 |
| `cursor` | El `next_cursor` de la respuesta anterior |

El cursor es opaco y queda atado a tu empresa, a tu ambiente, a la ruta y a sus filtros. No lo construyas ni lo edites: cambiar un filtro exige empezar de nuevo.

## Listas sin cursor

[`GET /v1/balances`](/docs/operaciones/listar-saldos/), [`GET /v1/wallets`](/docs/operaciones/listar-wallets/), [`GET /v1/receiving-accounts`](/docs/operaciones/listar-cuentas-para-recibir/), [`GET /v1/collection-methods`](/docs/operaciones/listar-metodos-de-cobro/) y [`GET /v1/webhook-endpoints`](/docs/operaciones/listar-destinos-de-webhook/) devuelven sólo `data`.

## Movimientos: un cursor que no termina

En [`GET /v1/movements`](/docs/operaciones/listar-movimientos/), `next_cursor` puede venir con valor aunque `has_more` sea `false`. Guárdalo en cada página: la siguiente consulta con ese cursor sigue donde quedaste.

Una vuelta es recorrer la lista hasta `has_more: false`. Al abrirla, el cursor fija su horizonte: la hora en que empezó. Lo conserva toda la vuelta, también a la mitad y aunque tardes horas en leerla. Al terminar, `next_cursor` no sigue desde la última fila: abre la siguiente vuelta desde ese horizonte, con un pequeño solape. Así ves lo que cambió mientras recorrías.

- Sólo `has_more` dice si sigue. Una página puede llegar corta, o vacía, con `has_more: true`.
- Un mismo `id` puede repetirse con un estado más nuevo. Deduplica por `id` y quédate con la copia de `updated_at` más reciente. Una `partial` no reemplaza a una `complete` que ya tienes.

Ver [Movimientos y recibos](/docs/recetas/movimientos-y-recibos/).

**Variables de estas muestras: simulador público, sin cuenta**

Define esto en tu terminal. Con estos valores las muestras corren tal cual contra el simulador público:

```bash
export FITAL_API="https://widget.fital.xyz/explorar/v1"
export FITAL_KEY="sk_test_sandbox_publica"
```

En la cuenta de prueba o en producción cambia la base, la llave y el secreto, y sustituye los `id` de ejemplo por los tuyos. Ver [Ambientes](/docs/ambientes/).

```bash
# CURSOR vive en tu almacenamiento durable; vacío la primera vez.
CURSOR=""
curl -sS -G "$FITAL_API/movements" \
  -H "Authorization: Bearer $FITAL_KEY" \
  --data-urlencode "limit=25" \
  ${CURSOR:+--data-urlencode "cursor=$CURSOR"}
```

```js
// cursor vive en tu almacenamiento durable; null la primera vez.
const cursor = null;
const q = new URLSearchParams({ limit: '25', ...(cursor ? { cursor } : {}) });
const r = await fetch(`${process.env.FITAL_API}/movements?${q}`, {
  headers: { Authorization: `Bearer ${process.env.FITAL_KEY}` },
});
const pagina = await r.json();
console.log(r.status, pagina.data.length, pagina.has_more, pagina.next_cursor);
```

```python
import json, os, urllib.error, urllib.parse, urllib.request
# cursor vive en tu almacenamiento durable; None la primera vez.
cursor = None
q = {"limit": "25", **({"cursor": cursor} if cursor else {})}
pet = urllib.request.Request(os.environ["FITAL_API"] + "/movements?" + urllib.parse.urlencode(q),
    headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"]})
try:
    with urllib.request.urlopen(pet) as r:
        pagina = json.load(r)
        print(r.status, len(pagina["data"]), pagina["has_more"], pagina["next_cursor"])
except urllib.error.HTTPError as e:
    print(e.code, e.read().decode())
```

```php
<?php
// $cursor vive en tu almacenamiento durable; null la primera vez.
$cursor = null;
$q = http_build_query(['limit' => 25] + ($cursor ? ['cursor' => $cursor] : []));
$c = curl_init(getenv('FITAL_API') . '/movements?' . $q);
curl_setopt_array($c, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('FITAL_KEY')],
]);
$pagina = json_decode(curl_exec($c), true);
echo count($pagina['data']), ' ', var_export($pagina['has_more'], true), ' ', $pagina['next_cursor'] ?? 'null', "\n";
```

## Errores comunes

- `400 invalid_request` por `limit` fuera de rango.
- Reusar un cursor con otros filtros: el cursor deja de ser válido.
- Terminar una vuelta por una página corta o vacía: sólo termina con `has_more: false`.

## Qué sigue

[Montos y fechas](/docs/montos-y-fechas/).
