Events
Every event the realtime connection sends, with its payload. Connect first; see Connecting.
Before you read the payloads
- Payloads are JSON strings. Parse them first. The shapes below are the parsed objects.
- Keys are snake_case, dates are ISO-8601 strings, and many flags are
0or1. - A key whose value is missing may be left out rather than sent as
null. The interfaces mark those keys with?. - Who receives what: every event below except
authorizedgoes to the lot it concerns and to the account that owns the lot. With an API token you get the events for your whole account; with a user token, those for the lots the user is assigned to. Something that isn't in a lot (a product with no lot, for example) sends nothing.
| Event | Sent when |
|---|---|
authorized | Your token is accepted |
lot_change | A command succeeded, or a Gateway reported a product's state |
gateway_error | A command failed |
gateway_status | A Gateway went online or offline |
plate_reading_create | An LPR camera read a plate |
plate_reading_update | An access decision was made for a reading |
session_create | A parking session started |
session_update | A parking session changed or ended |
lpr_camera_status | An LPR camera reported its status |
lot_detection_change | A lot's occupied count changed |
zone_change | A zone's occupied count changed |
parking_place_change | A parking place became free or occupied |
parking_gateway_state_change | A parking-detection gateway went online or offline |
lpr_event | Legacy: a lot entry was recorded from a plate reading |
Connection
authorized
Sent once, only to your connection, when the server accepts your token. The payload is an empty string. See Authentication.
Products and Gateways
lot_change
The state of one product. Despite the name, it's about a product, not a lot. It's sent in two cases:
- A command succeeded. After
POST /v2/products/{id}/stateorPOST /v2/products/{id}/settings, the result carries the sameevent_idthe endpoint returned. You may also seeevent_ids for operations you didn't start; ignore them. - A Gateway reported what it sees. These have no
event_id. They're sent for every product in each report, whether or not anything changed, and each one is sent twice. See Deduplicate status updates.
The product carries one block that matches its type: barrier, gate, chain,
bollard or brain.
interface LotChange {
lot_id: number | null;
event_id?: string; // 8 hex characters; only for command results
data: {
id: number;
device_id: string;
name: string;
lot_id: number | null;
product_type_id: number;
latitude: number | null;
longitude: number | null;
status: 'online' | 'offline';
battery_status: number | null;
rssi: number | null;
product_error_id?: number; // only when the product reports an error
product_error?: ProductError; // only when the product reports an error
deleted_at: null;
created_at: string;
updated_at: string;
barrier?: BarrierBlock;
gate?: BrainBlock;
chain?: BrainBlock;
bollard?: BrainBlock;
brain?: BrainBlock;
lot: {
id: number;
lot_cluster_id: number; // same as id
account_id: number;
name: string;
address: string;
latitude: number | null;
longitude: number | null;
timezone: string; // IANA name, e.g. "Europe/Zagreb"
created_at: string;
updated_at: string;
} | null;
};
}
interface BarrierBlock {
id: number;
product_id: number;
state: 'opened' | 'closed';
is_auto_close: 0 | 1; // REST calls this is_autoclose
is_car_on_top: 0 | 1;
auto_close_time: number; // seconds; REST calls this autoclose_time
create_at: string; // sic: REST calls this created_at
updated_at: string;
}
interface BrainBlock {
id: number;
product_id: number;
state: 'opened' | 'closed' | 'error' | 'unknown' | 'reserved' | 'disabled';
permanent_position: 'disabled' | 'stay_open' | 'stay_close';
deleted_at: string | null;
created_at: string;
updated_at: string;
}
interface ProductError {
id: number;
error_code: string;
jammed: 0 | 1;
sensor_dirty: 0 | 1;
pin_broken: 0 | 1;
rtcc_invalid: 0 | 1;
battery_empty: 0 | 1;
master_key_not_set: 0 | 1;
created_at: string;
updated_at: string;
}
For a barrier, opened means the arm is down (cars can pass over it) and closed
means the arm is up. See Products and operations.
:::warning The barrier block's keys differ from the REST API
In lot_change, the barrier block uses is_auto_close, auto_close_time and
create_at. The REST API's product responses use is_autoclose, autoclose_time and
created_at. Read the realtime keys as they are shown here.
:::
{
"lot_id": 7,
"event_id": "a1b2c3d4",
"data": {
"id": 123,
"device_id": "B1234567",
"name": "Entrance barrier",
"lot_id": 7,
"product_type_id": 2,
"latitude": 45.815,
"longitude": 15.982,
"status": "online",
"battery_status": 80,
"rssi": -70,
"deleted_at": null,
"created_at": "2026-01-15T09:30:00.000Z",
"updated_at": "2026-03-02T08:12:44.000Z",
"barrier": {
"id": 41,
"product_id": 123,
"state": "opened",
"is_auto_close": 1,
"is_car_on_top": 0,
"auto_close_time": 45,
"create_at": "2026-01-15T09:30:00.000Z",
"updated_at": "2026-03-02T08:12:44.000Z"
},
"lot": {
"id": 7,
"lot_cluster_id": 7,
"account_id": 3,
"name": "Main garage",
"address": "Example street 1",
"latitude": 45.815,
"longitude": 15.982,
"timezone": "Europe/Zagreb",
"created_at": "2026-01-10T10:00:00.000Z",
"updated_at": "2026-01-10T10:00:00.000Z"
}
}
}
gateway_error
A command failed. event_id matches the one
POST /v2/products/{id}/state or
POST /v2/products/{id}/settings returned.
See Asynchronous operations.
interface GatewayError {
type: number | 'Timeout'; // see the table below
event_id: string; // 8 hex characters
data: {
id: number; // product id
name: string; // product name
};
message?: string; // absent when type is "Timeout"
}
{
"type": 6,
"event_id": "a1b2c3d4",
"data": { "id": 123, "name": "Entrance barrier" },
"message": "Car on top"
}
When no Gateway got an answer from the product in time, type is the string
"Timeout" and there's no message. Otherwise type is one of these numbers. They are
not the HTTP error codes, even where the numbers overlap.
type | message |
|---|---|
0 | Auth error |
1 | Invalid guest key |
2 | Device disconnected |
3 | Bluetooth error |
4 | Device not found |
5 | Device jammed |
6 | Car on top |
7 | Sensor dirty |
8 | Device not initialized |
9 | Rtcc not set |
10 | Device busy |
11 | Unknown error |
gateway_status
A Gateway connected to or disconnected from Parklio. Only Gateways assigned to a lot
send it. The payload identifies the Gateway by its uuid, not its numeric id.
interface GatewayStatus {
uuid: string;
status: 'online' | 'offline';
}
{ "uuid": "xgobgwEG7rmZ", "status": "online" }
License plates and sessions
plate_reading_create
An LPR camera read a license plate. When it's sent, the access decision hasn't been made
yet: access_decision, access_decision_reason and session_id are null. Plate
readings created by
POST /v2/sessions don't send it.
interface PlateReading {
id: number;
plate: string;
confidence: number | null;
image_url: string | null; // temporary link to the full image
cropped_image_url: string | null; // temporary link to the plate crop
direction: 'in' | 'out' | 'undefined';
type: 'bus' | 'car' | 'motorcycle' | 'truck' | 'van' | null;
make: string | null;
color: string | null;
model: string | null;
country_code: string | null;
session_id: number | null;
access_decision: 'denied' | 'triggered' | null;
access_decision_reason:
| 'debounce'
| 'direction_mismatch'
| 'max_capacity'
| 'no_active_session'
| 'no_tariff'
| 'session_exit'
| 'tariff'
| 'unknown_direction'
| 'whitelist'
| null;
lpr_camera_id: number;
zone_id: number;
plate_side: 'front' | 'rear';
lpr_camera_direction: 'in' | 'out' | 'in/out'; // the camera's position at read time
created_at: string;
updated_at: string;
}
{
"id": 9001,
"plate": "ZG1234AB",
"confidence": 0.92,
"image_url": null,
"cropped_image_url": null,
"direction": "in",
"type": "car",
"make": null,
"color": null,
"model": null,
"country_code": "HR",
"session_id": null,
"access_decision": null,
"access_decision_reason": null,
"lpr_camera_id": 15,
"zone_id": 9,
"plate_side": "front",
"lpr_camera_direction": "in",
"created_at": "2026-03-02T08:12:40.000Z",
"updated_at": "2026-03-02T08:12:40.000Z"
}
direction is the string 'undefined' when the camera couldn't tell; such readings get
no access decision.
plate_reading_update
Same shape as plate_reading_create. Sent when:
- an access decision is recorded:
triggered(reasonwhitelist,tarifforsession_exit) ordenied(any other reason, includingdebouncefor a repeat read of the same plate); - the camera refines a reading after the fact (new plate, confidence or direction).
session_id is usually still null here: the decision is recorded before the reading
is linked to a session. To follow the session, listen for
session_create and session_update.
Editing a reading through
PATCH /v2/plate-readings/{id} doesn't send
this event.
session_create
A parking session started: either you or a user created it through
POST /v2/sessions, or an LPR camera let a vehicle in
(by whitelist or tariff).
interface Session {
id: number;
public_id: string; // 10 characters, A-Z and 0-9
ended_at?: string | null; // left out while the session is new
duration: number; // seconds
discount_time?: number | null; // minutes; -1 means unlimited
amount_paid: number;
possibly_exited?: boolean | null;
exit_allowed: boolean; // true while the session hasn't ended
entry_license_plate: {
id: number;
plate: string;
image_url: string | null;
cropped_image_url: string | null;
} | null;
created_at: string;
entry: { id: number; type: SessionSourceType };
exit?: { id: number; type: SessionSourceType } | null; // left out or null while open
zone_id: number;
}
// entry.id / exit.id is the id of that source: the camera, the account or the user
type SessionSourceType = 'lpr_camera' | 'account' | 'attendant' | 'terminal' | 'product' | 'payment';
entry.type is account when an API token created the session, attendant when a
user did, and lpr_camera when a camera let the vehicle in.
{
"id": 501,
"public_id": "K7Q2M9X4TB",
"duration": 0,
"amount_paid": 0,
"exit_allowed": true,
"entry_license_plate": {
"id": 9001,
"plate": "ZG1234AB",
"image_url": null,
"cropped_image_url": null
},
"created_at": "2026-03-02T08:12:41.000Z",
"entry": { "id": 15, "type": "lpr_camera" },
"zone_id": 9
}
session_update
Same shape as session_create. Sent when:
- a session is changed through
PATCH /v2/sessions/{id}(discount time or entry plate); - a session is closed through
POST /v2/sessions/{id}/close:ended_atandexitare set, withexit.typeaccountorattendant; - an LPR camera matches a vehicle leaving to an open session:
ended_atis set,possibly_exitedistrueandexit.typeislpr_camera.
lpr_camera_status
An LPR camera reported its status. It's sent for every camera in each report, even when the status didn't change, so keep the last value per camera. Not every camera model sends it.
interface LprCameraStatus {
id: number;
device_id: string;
status: 'online' | 'offline';
}
{ "id": 15, "device_id": "L123456789", "status": "online" }
Occupancy
A parking-detection sensor that changes state sends three events, in this order:
lot_detection_change, zone_change, then parking_place_change. Occupied counts
also change when LPR sessions start and end, and those changes don't send any
event; re-read the lot or zone over REST if you need exact counts.
lot_detection_change
interface LotDetectionChange {
lots: [
{
id: number; // lot id
detection_status: 'online';
occupied_count: number;
},
]; // always one element
}
{ "lots": [{ "id": 7, "detection_status": "online", "occupied_count": 42 }] }
zone_change
interface ZoneChange {
zones: [
{
id: number;
account_id: number;
lot_id: number;
name: string;
description: string | null;
spot_count: number;
occupied_count: number;
deleted_at: string | null;
created_at: string;
updated_at: string;
},
]; // always one element
}
{
"zones": [
{
"id": 9,
"account_id": 3,
"lot_id": 7,
"name": "Level -1",
"description": null,
"spot_count": 120,
"occupied_count": 42,
"deleted_at": null,
"created_at": "2026-01-10T10:00:00.000Z",
"updated_at": "2026-03-02T08:12:40.000Z"
}
]
}
parking_place_change
interface ParkingPlaceChange {
places: [
{
id: number;
lot_id: number;
zone_id: number;
name: string | null;
occupancy_status: 'free' | 'occupied';
},
]; // always one element
}
{
"places": [
{ "id": 301, "lot_id": 7, "zone_id": 9, "name": "Spot 12", "occupancy_status": "occupied" }
]
}
parking_gateway_state_change
A parking-detection gateway (not a product Gateway) went online or offline. Not every parking-detection gateway model sends it.
interface ParkingGatewayStateChange {
gateways: [{ id: number; status: 'online' | 'offline' }]; // always one element
}
{ "gateways": [{ "id": 4, "status": "offline" }] }
Legacy
lpr_event
:::warning Legacy
Use plate_reading_create and
plate_reading_update instead. lpr_event is still sent, but
lot entries are deprecated.
:::
Sent when a plate reading with a known direction, not debounced, is recorded as a lot
entry. An out entry that matches an earlier in has related_lot_entry and
duration set.
interface LprEvent {
lot_id: number;
data: LotEntry & {
lpr_camera: {
id: number;
device_id: string;
device_serial: string;
name: string;
account_id: number;
zone_id: number;
status: 'online' | 'offline' | 'auth' | 'sleep' | 'reset';
lpr_position: 'in' | 'out' | 'in/out';
connected_device_type: 'gate' | 'chain' | 'bollard' | 'other' | 'standalone';
mac: string;
deleted_at: string | null;
created_at: string | null;
updated_at: string | null;
};
related_lot_entry:
| (LotEntry & { lpr_camera: { id: number; device_id: string; name: string } })
| null;
};
}
interface LotEntry {
id: number;
scan_time: string;
lpr_camera_id: number;
lot_id: number;
related_lot_entry_id: number | null;
duration: number | null; // seconds; out entries only
is_allowed: 0 | 1;
direction: 'in' | 'out';
license_plate: string;
license_plate_image_url: string | null; // a storage key, not a usable URL
full_image_url: string | null; // a storage key, not a usable URL
make: string; // "" when unknown
color: string; // "" when unknown
model: string; // "" when unknown
color_depth: number | null;
created_at: string;
updated_at: string;
}
{
"lot_id": 7,
"data": {
"id": 320,
"scan_time": "2026-03-02T08:12:40.000Z",
"lpr_camera_id": 15,
"lot_id": 7,
"related_lot_entry_id": null,
"duration": null,
"is_allowed": 1,
"direction": "in",
"license_plate": "ZG1234AB",
"license_plate_image_url": null,
"full_image_url": null,
"make": "",
"color": "",
"model": "",
"color_depth": 1,
"created_at": "2026-03-02T08:12:40.000Z",
"updated_at": "2026-03-02T08:12:40.000Z",
"lpr_camera": {
"id": 15,
"device_id": "L123456789",
"device_serial": "123456789",
"name": "Entrance camera",
"account_id": 3,
"zone_id": 9,
"status": "online",
"lpr_position": "in",
"connected_device_type": "standalone",
"mac": "AA:BB:CC:DD:EE:FF",
"deleted_at": null,
"created_at": "2026-01-10T10:00:00.000Z",
"updated_at": "2026-01-10T10:00:00.000Z"
},
"related_lot_entry": null
}
}
Next
- Patterns: correlating results, deduplicating, resyncing.
- Asynchronous operations