# Live occupancy

> Show how full your lots, zones and parking places are, and keep the numbers live. This
> guide covers the counts, the per-place status, the realtime events, history and
> analytics, and public lot share views.

## Prerequisites

- An API token; see [Authentication](/api/authentication).
- A source of occupancy on the lot. Parking detection (cameras or sensors) is set up by
  Parklio technicians; you read the results, you don't configure detection. LPR cameras
  and payment machines also move the counts as vehicles enter and leave.
- For per-place status and history: the **`PARKING_DETECTION`** feature; see
  [Plans and permissions](/concepts/plans-and-permissions).

## Where the numbers come from

| Level | Fields | Updated by |
| --- | --- | --- |
| Lot | `spot_count`, `occupied_count` | Every change in one of its zones |
| Zone | `spot_count`, `occupied_count` | Detection sensors, LPR entries and exits, payment machines |
| Parking place | `occupancy_status`: `free`, `occupied` or `unknown` | Detection sensors only |

- Counts move by one on each entry or exit, and are kept between `0` and `spot_count`.
- On lots with parking detection, Parklio also recounts every **5 minutes** from the
  parking places: a zone's `occupied_count` becomes the number of its places that
  aren't `free`, and the lot's becomes the sum of its zones.
- Free spaces = `spot_count` − `occupied_count`.

## 1. Read the counts

```bash title="All zones of lot 7"
curl -s "https://api.parklio.com/v2/zones?filter=lot_id:eq:7&size=100" \
  -H "Authorization: Bearer $PARKLIO_API_TOKEN"
```

```json title="One zone in the response"
{
  "id": 9,
  "lot_id": 7,
  "name": "Level -1",
  "spot_count": 120,
  "occupied_count": 42
}
```

The lot totals are on [`GET /v2/lots/{id}`](/api/endpoints/lots-find-one).

## 2. Read parking places

With parking detection, each space is a **parking place** with its own status:

```bash title="Occupied places in zone 9"
curl -s "https://api.parklio.com/v2/parking-places?filter=zone_id:eq:9&filter=occupancy_status:eq:occupied&size=300" \
  -H "Authorization: Bearer $PARKLIO_API_TOKEN"
```

[`GET /v2/parking-places`](/api/endpoints/parking-places-find-all) can also filter by
`lot_id`, `name`, `uuid` and `external_id`. Without the `PARKING_DETECTION` feature,
every place's `occupancy_status` reads `unknown`.

## 3. Keep it live

Each time a detection sensor sees a change, three events arrive, in this order:

| Event | Contains |
| --- | --- |
| `lot_detection_change` | The lot's new `occupied_count` |
| `zone_change` | The zone, with `spot_count` and `occupied_count` |
| `parking_place_change` | The place and its `occupancy_status` |

Shapes are in [Events](/realtime/events#occupancy).

```ts title="live-occupancy.ts"
const zones = new Map<number, { spot_count: number; occupied_count: number }>();

socket.on('zone_change', (raw: string) => {
  for (const zone of JSON.parse(raw).zones) {
    zones.set(zone.id, { spot_count: zone.spot_count, occupied_count: zone.occupied_count });
    render(zone.id); // your UI
  }
});

function render(zoneId: number) {
  const zone = zones.get(zoneId);
  if (zone) console.log(`zone ${zoneId}: ${zone.spot_count - zone.occupied_count} free`);
}
```

:::warning Not every change is pushed
Count changes from **LPR entries and exits** and **payment machines**, and the 5-minute
recount, send **no** event. If your lots count vehicles that way, re-read the zones
every minute or so, as well as listening for events. Always re-read after a reconnect.
:::

## 4. Set counts from your own system

If you count vehicles yourself, you can write a zone's counts with
[`PATCH /v2/zones/{id}`](/api/endpoints/zones-update); the lot follows by the
difference:

```bash title="Set zone 9 to 40 occupied"
curl -s -X PATCH https://api.parklio.com/v2/zones/9 \
  -H "Authorization: Bearer $PARKLIO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"occupied_count": 40}'
```

- Keep `occupied_count` at or below `spot_count` yourself; this call doesn't enforce it.
- On lots with parking detection, the 5-minute recount overwrites what you set.
- Lot counts can't be set directly.

## 5. History and analytics

- [`GET /v2/parking-place-changes`](/api/endpoints/parking-place-changes-find-all): every
  status change of every place, with its `duration` in seconds. Filter by `lot_id`,
  `zone_id`, `parking_place_id`, `occupancy_status` or `created_at`;
  [export as CSV](/api/endpoints/parking-place-changes-export-to-csv). Needs
  `PARKING_DETECTION`.
- [`GET /v2/parking-analytics`](/api/endpoints/parking-analytics-get-analytics): average
  and total occupancy duration, turnover and peak hour, per lot, zone or place, over a
  date range, optionally per hour, day or month. Every option is a `filter`:

```bash title="Daily turnover per zone for March"
curl -s "https://api.parklio.com/v2/parking-analytics?filter=metric:eq:turnover_rate&filter=scope:eq:zone&filter=interval:eq:day&filter=lot_ids:eq:7&filter=created_at:gte:2026-03-01T00:00:00.000Z&filter=created_at:lte:2026-04-01T00:00:00.000Z" \
  -H "Authorization: Bearer $PARKLIO_API_TOKEN"
```

`lot_ids`, `zone_ids` and `parking_place_ids` take comma-separated ids, such as
`lot_ids:eq:7,8`. Metrics: `average_duration`, `average_duration_percentage`, `total_duration`,
`turnover_rate` and `peak_occupancy_hour` (which can't be combined with `interval`).

## 6. Share a live view

A **lot share view** is a public web page showing live occupancy for some zones of a
lot, with no login. Use it for a screen at the entrance, or a link on your website.

```bash title="Create a share view"
curl -s -X POST https://api.parklio.com/v2/lot-share-views \
  -H "Authorization: Bearer $PARKLIO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"lot_id": 7, "zone_ids": [9, 10], "lot_share_view_type": "counts_only"}'
```

- The response's **`url`** is the public page. Anyone with it can see the page, so share
  it as you would a password.
- `lot_share_view_type`: `counts_only` (free, occupied and total per zone),
  `live_camera` (adds a camera frame; needs a parking-detection camera in a zone) or
  `vector_map` (adds a map of the places; needs zones with a map).
- Add `?navbar=hidden` or `?darkmode=true` to the URL to change how it looks.
- To turn a link off, revoke it:
  [`PATCH /v2/lot-share-views/{id}`](/api/endpoints/lot-share-views-update) with
  `{"revoked": true}`. The page then returns 404.

## Verify it worked

1. `GET /v2/zones?filter=lot_id:eq:<lot>` shows `spot_count` and `occupied_count` for
   each zone.
2. Park a car on a detected space: `lot_detection_change`, `zone_change` and
   `parking_place_change` arrive, and `occupied_count` is one higher.
3. The share view's `url` opens in a browser without logging in.

## Common errors

| Symptom | Cause | Fix |
| --- | --- | --- |
| Every place is `unknown` | No `PARKING_DETECTION` feature. | See [Plans and permissions](/concepts/plans-and-permissions). |
| Counts change but no event arrives | The change came from LPR, a payment machine or the recount. | Poll zones as well (step 3). |
| A count you set came back different | The 5-minute recount on a detection lot. | Don't set counts on detection lots. |
| `403` code `30` on place history | No `PARKING_DETECTION` feature. | As above. |
| `400` code `33` | A filter that endpoint doesn't accept. | Check the endpoint's filters. |
| Share view returns 404 | It was revoked. | Create a new one. |

## Next

- [Events](/realtime/events#occupancy)
- [Sessions and tariffs](/guides/sessions-and-tariffs)
