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

# Cashless

> Saldos, consumiciones, recargas y conciliación.

Dentro del recinto no circula dinero: la pulsera lleva saldo, se recarga y se
gasta en las barras. Esta sección cubre cómo consultarlo y, si el organizador te
lo autoriza, cómo recargarlo.

**Permisos:** `cashless.read` para todo lo de lectura, `cashless.write` para
recargar (requiere activación manual).

<Note>
  Todos los importes son **enteros en céntimos**. `2500` son 25,00 €. La moneda del
  evento está en `GET /event`.
</Note>

## Consultar una pulsera

Se direcciona por su código de radio, NFC o UHF, que es lo que tu aplicación
tiene tras un escaneo:

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.ventry.es/v1/wristbands/04A2B1C3D4E580 \
    -H "Authorization: Bearer $VENTRY_KEY"
  ```

  ```js Node.js theme={null}
  const wristband = await (
    await fetch("https://api.ventry.es/v1/wristbands/04A2B1C3D4E580", {
      headers: { Authorization: `Bearer ${process.env.VENTRY_KEY}` },
    })
  ).json();

  console.log(wristband.balance);  // en céntimos
  ```

  ```python Python theme={null}
  wristband = requests.get(
      "https://api.ventry.es/v1/wristbands/04A2B1C3D4E580",
      headers={"Authorization": f"Bearer {os.environ['VENTRY_KEY']}"},
      timeout=10,
  ).json()

  print(wristband["balance"])  # en céntimos
  ```

  ```php PHP theme={null}
  $wristband = callVentry("GET", "/wristbands/04A2B1C3D4E580");

  echo $wristband["balance"];  // en céntimos
  ```
</CodeGroup>

```json theme={null}
{
  "object": "wristband",
  "id": "6a3d0712a1e963300ee2cc21",
  "nfc": "04A2B1C3D4E580",
  "uhf": "E28068940000501234567890",
  "status": "active",
  "balance": 4750,
  "attendee": { "name": "Ada Lovelace", "anonymous": false },
  "ticket": { "code": "TR-84213-001", "ticket_type": "Abono VIP" },
  "allowed_zones": [
    { "id": "6a3d2a67a1e963300ee2c99c", "name": "Recinto" },
    { "id": "6a3d2a6ea1e963300ee2c99d", "name": "Zona VIP" }
  ],
  "valid_days": ["6a3d0512...", "6a3d0518..."],
  "created_at": "2026-06-12T17:20:41.882Z",
  "updated_at": "2026-06-12T22:05:13.119Z"
}
```

Estados posibles:

| `status`         | Significado                                      |
| ---------------- | ------------------------------------------------ |
| `active`         | Operativa                                        |
| `blocked`        | Bloqueada por el organizador                     |
| `lost`           | Dada por perdida                                 |
| `released`       | Liberada al final del evento                     |
| `pending_review` | Doble acreditación sin resolver: no puede gastar |

<Note>
  Las pulseras **anónimas** —las que se venden en el punto de recarga sin entrada
  asociada— devuelven `attendee.name: null` y `anonymous: true`. No tienen un
  titular identificado, y el nombre interno que llevan es sintético.
</Note>

### Saldo negativo

`balance` **puede ser negativo**. No es un error de tu lado: cuando un terminal
cobra sin cobertura y la venta se sincroniza más tarde, la consumición ya se
sirvió y VENTRY prefiere registrar el descubierto a descuadrar la caja. El
descubierto está acotado por la política del evento.

## Consumiciones

```bash theme={null}
curl "https://api.ventry.es/v1/wristbands/04A2B1C3D4E580/transactions?limit=50" \
  -H "Authorization: Bearer $VENTRY_KEY"
```

```json theme={null}
{
  "object": "list",
  "data": [
    {
      "object": "transaction",
      "id": "6a3d0911a1e963300ee2ce02",
      "status": "completed",
      "type": "cart",
      "payment_method": "wristband",
      "total": 750,
      "total_base": 682,
      "total_tax": 68,
      "tax_breakdown": [{ "tax_type": "IVA", "rate": 10, "base": 682, "amount": 68 }],
      "items": [
        {
          "product_id": "6a3d2b11a1e963300ee2ca02",
          "name": "Cerveza",
          "quantity": 2,
          "unit_price": 500,
          "charged_amount": 500,
          "included_quantity": 1
        }
      ],
      "wristband_id": "6a3d0712a1e963300ee2cc21",
      "day_id": "6a3d0512...",
      "device_id": "6a3d0620a1e963300ee2cb99",
      "occurred_at": "2026-06-12T21:14:55.301Z",
      "created_at": "2026-06-12T21:15:02.771Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

<Warning>
  Fíjate en la diferencia entre `quantity` y `charged_amount`. En el ejemplo se
  sirvieron **dos** cervezas de 5,00 €, pero una estaba cubierta por una
  consumición incluida en el abono VIP (`included_quantity: 1`), así que sólo se
  cobraron 5,00 € al saldo. **`total` es siempre lo realmente cobrado**, no el
  valor nominal de lo servido. Si sumas `unit_price * quantity` para cuadrar caja,
  te saldrán ingresos que nunca existieron.
</Warning>

## Recargas y retiradas

```bash theme={null}
curl "https://api.ventry.es/v1/topups?day=6a3d0512...&direction=topup&limit=200" \
  -H "Authorization: Bearer $VENTRY_KEY"
```

```json theme={null}
{
  "object": "list",
  "data": [
    {
      "object": "topup",
      "id": "6a3d0a02a1e963300ee2cf13",
      "direction": "topup",
      "amount": 2000,
      "previous_balance": 2750,
      "new_balance": 4750,
      "payment_method": "card",
      "status": "completed",
      "wristband_id": "6a3d0712a1e963300ee2cc21",
      "day_id": "6a3d0512...",
      "device_id": "6a3d0633a1e963300ee2cba7",
      "occurred_at": "2026-06-12T20:02:19.550Z",
      "created_at": "2026-06-12T20:02:19.812Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

`direction: "withdraw"` son devoluciones de saldo al asistente. `amount` es
siempre positivo; el sentido lo da `direction`.

## Recargar saldo

Sólo si tienes `cashless.write`. Pensado para integradores que cobran la recarga
en su propia plataforma —una app de recarga online, por ejemplo— y necesitan
reflejarla en VENTRY.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.ventry.es/v1/wristbands/04A2B1C3D4E580/topups \
    -H "Authorization: Bearer $VENTRY_KEY" \
    -H "Idempotency-Key: recarga-99312" \
    -H "Content-Type: application/json" \
    -d '{ "amount": 2500 }'
  ```

  ```js Node.js theme={null}
  // Llamar SÓLO después de que tu pasarela confirme el cobro.
  await fetch("https://api.ventry.es/v1/wristbands/04A2B1C3D4E580/topups", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.VENTRY_KEY}`,
      // Derivada del pago, no aleatoria: sobrevive a un reinicio del proceso.
      "Idempotency-Key": `recarga-${payment.id}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ amount: 2500 }),  // céntimos
  });
  ```

  ```python Python theme={null}
  # Llamar SÓLO después de que tu pasarela confirme el cobro.
  requests.post(
      "https://api.ventry.es/v1/wristbands/04A2B1C3D4E580/topups",
      headers={
          "Authorization": f"Bearer {os.environ['VENTRY_KEY']}",
          # Derivada del pago, no aleatoria: sobrevive a un reinicio del proceso.
          "Idempotency-Key": f"recarga-{payment.id}",
      },
      json={"amount": 2500},  # céntimos
      timeout=30,
  )
  ```

  ```php PHP theme={null}
  <?php
  // Llamar SÓLO después de que tu pasarela confirme el cobro.
  callVentry("POST", "/wristbands/04A2B1C3D4E580/topups", [
      "headers" => [
          // Derivada del pago, no aleatoria: sobrevive a un reinicio del proceso.
          "Idempotency-Key: recarga-{$payment->id}",
          "Content-Type: application/json",
      ],
      "body" => json_encode(["amount" => 2500]),  // céntimos
  ]);
  ```
</CodeGroup>

```json theme={null}
{
  "object": "topup_result",
  "id": "6a3d0b41a1e963300ee2d022",
  "amount": 2500,
  "previous_balance": 4750,
  "new_balance": 7250,
  "replayed": false
}
```

<Warning>
  **Cobra tú primero, recarga después.** VENTRY no cobra nada: se limita a añadir
  saldo. Llama a este endpoint sólo cuando tu pasarela haya confirmado el pago,
  nunca antes.

  El movimiento se registra con método de pago `manual` y contra el dispositivo
  virtual de tu API key, así que **aparece en el cierre de caja del evento**. El
  organizador tiene que poder verlo para cuadrar: el dinero lo tienes tú.
</Warning>

Si la pulsera no admite recargas —está en revisión, o liberada— la respuesta es
`422 topup_failed`.

## Conciliación

Para volcar la facturación a tu propio sistema:

```bash theme={null}
curl "https://api.ventry.es/v1/reports/sales-summary?day=6a3d0512..." \
  -H "Authorization: Bearer $VENTRY_KEY"
```

```json theme={null}
{
  "object": "sales_summary",
  "day_id": "6a3d0512...",
  "currency": "EUR",
  "sales": {
    "count": 4182,
    "total": 3187450,
    "total_tax": 289768,
    "by_method": { "wristband": 2984200, "card": 203250 }
  },
  "topups": {
    "count": 1904,
    "total": 3402000,
    "by_method": { "cash": 1201000, "card": 2151000, "manual": 50000 }
  },
  "withdrawals": { "count": 112, "total": 89400 },
  "unconsumed_balance": 124600
}
```

Estas cifras salen de la misma función que alimenta el cierre de caja del panel
de VENTRY, así que **coinciden exactamente** con lo que ve el organizador.

<AccordionGroup>
  <Accordion title="Agrupa siempre por occurred_at">
    Una venta hecha a las 22:00 sin cobertura y sincronizada a las 02:00 lleva
    `occurred_at` de las 22:00. Si agrupas por `created_at`, la facturación de la
    noche del viernes aparecerá partida entre viernes y sábado.
  </Accordion>

  <Accordion title="Solapa la ventana al sincronizar">
    Por lo mismo: retrocede unas horas respecto a tu última pasada y deduplica
    por `id`. Ver [Paginación](/pagination).
  </Accordion>

  <Accordion title="unconsumed_balance no es ingreso">
    Es el saldo que queda en las pulseras sin gastar. Parte se devolverá al
    asistente y parte no. No lo sumes a las ventas.
  </Accordion>

  <Accordion title="Si tu clave está limitada a días concretos">
    `GET /reports/sales-summary` sin `day` responde `422 day_required`. Es
    deliberado: devolverte un total recortado en silencio parecería el total del
    evento y no lo sería. Pide cada día por separado.
  </Accordion>
</AccordionGroup>
