> ## 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

> Balances, purchases, top-ups and reconciliation.

No cash circulates inside the venue: the wristband carries balance, it is topped
up and spent at the bars. This page covers how to read it and, if the organiser
authorises you, how to top it up.

**Scopes:** `cashless.read` for everything read-only, `cashless.write` to top up
(requires manual activation).

<Note>
  All amounts are **integers in cents**. `2500` is €25.00. The event's currency is
  in `GET /event`.
</Note>

## Reading a wristband

It is addressed by its radio code, NFC or UHF, which is what your application has
after a scan:

<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);  // in cents
  ```

  ```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"])  # in cents
  ```

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

  echo $wristband["balance"];  // in cents
  ```
</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": "VIP pass" },
  "allowed_zones": [
    { "id": "6a3d2a67a1e963300ee2c99c", "name": "Venue" },
    { "id": "6a3d2a6ea1e963300ee2c99d", "name": "VIP area" }
  ],
  "valid_days": ["6a3d0512...", "6a3d0518..."],
  "created_at": "2026-06-12T17:20:41.882Z",
  "updated_at": "2026-06-12T22:05:13.119Z"
}
```

Possible statuses:

| `status`         | Meaning                                       |
| ---------------- | --------------------------------------------- |
| `active`         | Operational                                   |
| `blocked`        | Blocked by the organiser                      |
| `lost`           | Reported lost                                 |
| `released`       | Released at the end of the event              |
| `pending_review` | Unresolved double accreditation: cannot spend |

<Note>
  **Anonymous** wristbands — the ones sold at the top-up point with no ticket
  attached — return `attendee.name: null` and `anonymous: true`. They have no
  identified holder, and the internal name they carry is synthetic.
</Note>

### Negative balance

`balance` **can be negative**. It is not a bug on your side: when a terminal
charges without connectivity and the sale syncs later, the drink has already been
served, and VENTRY would rather record the overdraft than unbalance the till. The
overdraft is capped by the event's policy.

## Purchases

```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": "Beer",
          "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>
  Note the difference between `quantity` and `charged_amount`. In the example
  **two** €5.00 beers were served, but one was covered by a drink included in the
  VIP pass (`included_quantity: 1`), so only €5.00 was charged to the balance.
  **`total` is always what was actually charged**, not the nominal value of what
  was served. If you add up `unit_price * quantity` to reconcile takings, you will
  book revenue that never existed.
</Warning>

## Top-ups and withdrawals

```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"` entries are balance refunds to the attendee. `amount` is
always positive; the direction is given by `direction`.

## Topping up

Only if you have `cashless.write`. Meant for integrators who charge the top-up on
their own platform — an online top-up app, for instance — and need to reflect it
in 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: topup-99312" \
    -H "Content-Type: application/json" \
    -d '{ "amount": 2500 }'
  ```

  ```js Node.js theme={null}
  // Call ONLY after your payment gateway has confirmed the charge.
  await fetch("https://api.ventry.es/v1/wristbands/04A2B1C3D4E580/topups", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.VENTRY_KEY}`,
      // Derived from the payment, not random: survives a process restart.
      "Idempotency-Key": `topup-${payment.id}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ amount: 2500 }),  // cents
  });
  ```

  ```python Python theme={null}
  # Call ONLY after your payment gateway has confirmed the charge.
  requests.post(
      "https://api.ventry.es/v1/wristbands/04A2B1C3D4E580/topups",
      headers={
          "Authorization": f"Bearer {os.environ['VENTRY_KEY']}",
          # Derived from the payment, not random: survives a process restart.
          "Idempotency-Key": f"topup-{payment.id}",
      },
      json={"amount": 2500},  # cents
      timeout=30,
  )
  ```

  ```php PHP theme={null}
  <?php
  // Call ONLY after your payment gateway has confirmed the charge.
  callVentry("POST", "/wristbands/04A2B1C3D4E580/topups", [
      "headers" => [
          // Derived from the payment, not random: survives a process restart.
          "Idempotency-Key: topup-{$payment->id}",
          "Content-Type: application/json",
      ],
      "body" => json_encode(["amount" => 2500]),  // cents
  ]);
  ```
</CodeGroup>

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

<Warning>
  **Charge first, top up after.** VENTRY charges nothing: it only adds balance.
  Call this endpoint once your gateway has confirmed the payment, never before.

  The movement is recorded with payment method `manual` and against your API key's
  virtual device, so it **shows up in the event's closeout**. The organiser has to
  be able to see it in order to reconcile: you are the one holding the money.
</Warning>

If the wristband does not accept top-ups — it is under review, or released — the
response is `422 topup_failed`.

## Reconciliation

To pull the takings into your own system:

```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
}
```

These figures come from the same function that feeds the closeout in the VENTRY
dashboard, so they **match exactly** what the organiser sees.

<AccordionGroup>
  <Accordion title="Always group by occurred_at">
    A sale made at 22:00 without connectivity and synced at 02:00 carries an
    `occurred_at` of 22:00. If you group by `created_at`, Friday night's takings
    will appear split between Friday and Saturday.
  </Accordion>

  <Accordion title="Overlap the window when syncing">
    For the same reason: go back a few hours from your last pass and deduplicate
    by `id`. See [Pagination](/en/pagination).
  </Accordion>

  <Accordion title="unconsumed_balance is not revenue">
    It is the balance left unspent on wristbands. Some will be refunded to
    attendees and some will not. Do not add it to sales.
  </Accordion>

  <Accordion title="If your key is limited to specific days">
    `GET /reports/sales-summary` without `day` returns `422 day_required`. This is
    deliberate: silently handing you a truncated total would look like the whole
    event's total and would not be. Request each day separately.
  </Accordion>
</AccordionGroup>
