# Sessions and tariffs

> A **session** is one vehicle's stay in a zone, from entry to exit. **Tariffs** decide
> which vehicles an LPR camera lets in when they aren't on the
> [whitelist](/guides/plate-based-access), and how the stay is priced. This guide covers
> reading and managing sessions, and setting up tariffs, members and groups.

## Prerequisites

- An API token; see [Authentication](/api/authentication).
- For automatic sessions: LPR cameras assigned to zones, on the way in and the way out.

## How sessions start and end

| Event | Session | `entry.type` / `exit.type` |
| --- | --- | --- |
| A camera lets a vehicle in (whitelist or tariff) | Starts | `lpr_camera` |
| You create one with [`POST /v2/sessions`](/api/endpoints/sessions-create) | Starts | `account` (API token) or `attendant` (user) |
| A camera sees the vehicle leave | Ends; `possibly_exited` becomes `true` | `lpr_camera` |
| You close it with [`POST /v2/sessions/{id}/close`](/api/endpoints/sessions-close) | Ends | `account` or `attendant` |

Payment machines also create and close sessions, and record payments in `amount_paid`.

Session fields you'll use most:

| Field | Meaning |
| --- | --- |
| `public_id` | A 10-character code, A–Z and 0–9, for showing to the driver. Filterable. |
| `duration` | Seconds so far, or the full stay once ended. |
| `ended_at` | When it ended; absent or `null` while open. |
| `exit_allowed` | `true` while the session is open. |
| `discount_time` | Free minutes granted, or `-1` for unlimited. |
| `amount_paid` | What was paid at a payment machine. Read-only. |
| `entry_license_plate` | The plate read on entry. |

## 1. Follow sessions

In realtime, `session_create` and `session_update` carry the whole session; see
[Events](/realtime/events#session_create). Over REST,
[`GET /v2/sessions`](/api/endpoints/sessions-find-all) filters by `lot_id`, `zone_id`,
`license_plate`, `public_id`, `lpr_camera_id`, `created_at` and `ended_at`:

```bash title="Open sessions in lot 7"
curl -s "https://api.parklio.com/v2/sessions?filter=lot_id:eq:7&filter=ended_at:eq:null&sort=created_at:desc" \
  -H "Authorization: Bearer $PARKLIO_API_TOKEN"
```

For reports, [`GET /v2/sessions/export`](/api/endpoints/sessions-export-to-csv) returns a
CSV, [`GET /v2/sessions/analytics`](/api/endpoints/sessions-get-analytics) counts entries
and exits (or the peak hour) per lot or zone, and
[`GET /v2/sessions/vehicle-analytics`](/api/endpoints/sessions-vehicles-analytics) gives
visits and time spent per plate.

## 2. Create and change sessions

Create a session for a vehicle a camera missed, or one you let in by hand. Name the
camera: the session is created in that camera's zone.

```bash title="Start a session"
curl -s -X POST https://api.parklio.com/v2/sessions \
  -H "Authorization: Bearer $PARKLIO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"license_plate": "ZG1234AB", "lpr_camera_id": 15}'
```

- Always send both `license_plate` and `lpr_camera_id`.
- No whitelist, tariff or capacity check is made, and the zone's `occupied_count`
  doesn't change.

Grant free time, or fix a misread plate, with
[`PATCH /v2/sessions/{id}`](/api/endpoints/sessions-update):

```json title="Grant 60 free minutes"
{ "discount_time": 60 }
```

`discount_time` is in minutes; `-1` means unlimited. A value of `0` is ignored. To
correct the entry plate, send `license_plate` instead.

End a session with [`POST /v2/sessions/{id}/close`](/api/endpoints/sessions-close) (no
body). Close only open sessions (check `ended_at` first): closing doesn't check whether
the session already ended, and doesn't change the zone's `occupied_count`.

## 3. Set up tariffs

A tariff is a set of rules for a zone: when it applies, who it applies to, and how the
stay is priced. Create one with [`POST /v2/tariffs`](/api/endpoints/tariffs-create);
only `name` is required:

```bash title="Weekday tariff, 2 per hour after 15 free minutes"
curl -s -X POST https://api.parklio.com/v2/tariffs \
  -H "Authorization: Bearer $PARKLIO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Weekdays",
    "interval": 60,
    "price": 2,
    "entry_free_time_interval": 15,
    "price_progression_type": "flat",
    "schedules": [
      { "weekday": 0, "start_time": "07:00:00", "end_time": "19:00:00" },
      { "weekday": 1, "start_time": "07:00:00", "end_time": "19:00:00" },
      { "weekday": 2, "start_time": "07:00:00", "end_time": "19:00:00" },
      { "weekday": 3, "start_time": "07:00:00", "end_time": "19:00:00" },
      { "weekday": 4, "start_time": "07:00:00", "end_time": "19:00:00" }
    ]
  }'
```

| Field | Meaning |
| --- | --- |
| `interval`, `price` | Price per interval of minutes (at least 15; default 60). |
| `price_progression_type` | `flat` (default), `linear`, `exponential` or `incremental`. |
| `entry_free_time_interval`, `exit_free_time_interval`, `gratis_time` | Free minutes. |
| `maxima`, `max_time_of_stay` | Maximum price, maximum stay in minutes. |
| `schedules` | Up to 7 `{weekday, start_time, end_time}`; `weekday` 0 is **Monday**. Times are in the lot's time zone. |
| `start_time`, `end_time` | Absolute dates the tariff is valid between. |
| `season_start`, `season_end` | A yearly season (`YYYY-MM-DD`; only day and month count). |
| `group_ids`, `max_affected_members_per_group` | Limit the tariff to groups, and how many of a group's members can be inside at once. |
| `currency_id` | Defaults to EUR. |

Then assign the tariff to a zone with
[`PUT /v2/zones/{id}/tariffs/{tariffId}`](/api/endpoints/zones-assign-member). A zone
can have several tariffs; their order is set with
[`PATCH /v2/zones/{zone_id}/tariffs/{tariff_id}`](/api/endpoints/zones-update-order) and
`{"order": 0}`. A tariff can't be deleted while a zone still uses it (code `1400`).

The API doesn't calculate prices; payment machines do, from these rules.

## 4. Members and groups

To restrict a tariff to known vehicles, register their plates as **members**
([`POST /v2/members`](/api/endpoints/members-create), or a
[CSV upload](/api/endpoints/members-create-from-file)), put them in a **group**
([`POST /v2/groups`](/api/endpoints/groups-create)), and give the tariff that group's id
in `group_ids`. A tariff with no groups applies to every vehicle.

Members and groups don't open anything by themselves; they only decide which tariffs
apply. For access that doesn't depend on tariffs, use the
[whitelist](/guides/plate-based-access).

## How a camera decides on entry

For a plate that isn't on the lot's whitelist:

1. Find the zone's tariffs that apply **now** (dates, season, schedule) and to **this
   plate** (no groups, or a group the plate is a member of).
2. None: denied, reason `no_tariff`.
3. If every one of them is full (its group already has
   `max_affected_members_per_group` members with open sessions): denied, reason
   `max_capacity`.
4. Otherwise: let in, reason `tariff`, and a session starts.

On the way out, a vehicle with an open session is let out (`session_exit`) and its
session ends; one without is denied (`no_active_session`). The zone's
`occupied_count` isn't part of the decision.

## Verify it worked

1. Create a tariff for a zone with a schedule covering now, and no groups.
2. Drive in past the zone's entry camera: `plate_reading_update` shows
   `access_decision: "triggered"`, reason `tariff`, and `session_create` arrives.
3. `GET /v2/sessions?filter=public_id:eq:<public_id>` returns the session, with no
   `ended_at`.
4. Drive out: `session_update` arrives with `ended_at` set and `exit.type`
   `lpr_camera`.

## Common errors

| Symptom | Cause | Fix |
| --- | --- | --- |
| Reason `no_tariff` | No tariff applies now, or none matches the plate's groups. | Check the zone's tariffs, schedules (Monday = 0, lot time) and groups. |
| Reason `max_capacity` | Every applicable tariff's group is full. | Raise `max_affected_members_per_group`, or close stale sessions. |
| Reason `no_active_session` on exit | The entry wasn't recorded as a session. | Create the session by hand, or check the entry camera. |
| `discount_time: 0` has no effect | `0` is ignored. | Not supported today. |
| `1400` when deleting a tariff | A zone still uses it. | Unassign it from its zones first. |
| `1505` when correcting a plate | The session has no entry plate. | Not possible for that session. |

## Next

- [Plate-based access](/guides/plate-based-access)
- [Live occupancy](/guides/live-occupancy)
