# Movimientos y recibos

Lista cada entrada y salida de tu cuenta y descarga su recibo.

[`GET /v1/movements`](/docs/operaciones/listar-movimientos/) es el historial de tu cuenta. [`GET /v1/movements/{id}/receipt`](/docs/operaciones/descargar-recibo/) da el recibo de un movimiento terminal, en PDF o en JSON.

## Listar

Filtros: `from`, `to`, `type` y `currency`. Los tipos son `payment`, `deposit`, `crypto`, `internal`, `card` y `other`: cada uno se explica en [Valores de `type`](/docs/operaciones/listar-movimientos/#valores-de-type).

**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";
```

Guarda `next_cursor` en cada página, aunque `has_more` sea `false`: es el punto desde el que seguirás mañana. 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. Ver [Paginación](/docs/paginacion/#movimientos-un-cursor-que-no-termina).

## Lectura parcial

`read_status: "partial"` significa que el dato aún no está completo. Relee ese movimiento más tarde, y no dejes que reemplace a una fila `complete` que ya tengas. Con `partial` no concluyas nada de `status`: suele salir `processing` aunque el pago ya hubiera terminado, y eso no es un retroceso.

## Recibo

Con `Accept: application/json` obtienes los datos. Sin ese encabezado obtienes un PDF. La respuesta no se debe guardar en cachés compartidos.

```bash
curl -sS "$FITAL_API/movements/mov_2hKsV9tBq4XzM7nD1cYpL6rF/receipt" \
  -H "Authorization: Bearer $FITAL_KEY" \
  -H "Accept: application/json"
```

```js
const r = await fetch(`${process.env.FITAL_API}/movements/mov_2hKsV9tBq4XzM7nD1cYpL6rF/receipt`, {
  headers: { Authorization: `Bearer ${process.env.FITAL_KEY}`, 'Accept': 'application/json' },
});
console.log(r.status, await r.json());
```

```python
import json, os, urllib.error, urllib.request
pet = urllib.request.Request(os.environ["FITAL_API"] + "/movements/mov_2hKsV9tBq4XzM7nD1cYpL6rF/receipt",
    headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"], "Accept": "application/json"})
try:
    with urllib.request.urlopen(pet) as r:
        print(r.status, json.load(r))
except urllib.error.HTTPError as e:
    print(e.code, e.read().decode())
```

```php
<?php
$c = curl_init(getenv('FITAL_API') . '/movements/mov_2hKsV9tBq4XzM7nD1cYpL6rF/receipt');
curl_setopt_array($c, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('FITAL_KEY'), 'Accept: application/json'],
]);
echo curl_exec($c), "\n";
```

Si el movimiento aún no es terminal, la respuesta es `409 receipt_not_available`. `completed_at` llega en `null` si el movimiento no terminó en `completed`: un recibo de `failed`, `returned` o `canceled` no trae fecha de completado.

## Errores comunes

- Pedir el recibo de un movimiento en `processing`.
- Reiniciar el cursor cada día: vuelves a leer todo.
- Guardar el cursor sólo al terminar la vuelta: si tu proceso se cae, la repites entera.
- `403 forbidden_scope`: necesitas `movements:read`.

## Qué sigue

[Conciliar](/docs/recetas/conciliar/).
