Examples
Complete scripts for the common scenarios, in Node.js (TypeScript) and Python. Each one is type-checked or compiled on every docs build, and the code on the guide pages comes from the same files.
Setup
- An API token; see Authentication. The scripts read it from
PARKLIO_API_TOKEN, and ids fromPRODUCT_IDandLOT_ID. - Node.js 24+, which runs TypeScript files directly, and
socket.io-client4. - Python 3.10+, with
httpxandpython-socketio5.
npm install socket.io-client # Node.js
pip install httpx "python-socketio[client]" # Python
export PARKLIO_API_TOKEN="<your-api-token>"
export PRODUCT_ID=123
export LOT_ID=7
Run a script with node open.ts or python3 open.py.
Open a product and wait for the result
Connect to realtime, send open, and wait for the lot_change or gateway_error with
your event_id. Explained in the Quickstart.
- Node.js
- Python
import { io } from 'socket.io-client';
const token = process.env.PARKLIO_API_TOKEN;
const productId = process.env.PRODUCT_ID;
interface OperationEvent {
event_id?: string;
type?: number | 'Timeout';
message?: string;
data: Record<string, any>;
}
type Result = { ok: boolean; event: OperationEvent };
// 1. Connect first. `connect` fires once the server has accepted the token.
const socket = io('https://api.parklio.com/v2/realtime', {
path: '/socketio',
query: { token },
});
await new Promise((resolve, reject) => {
socket.once('connect', () => resolve(undefined));
socket.once('connect_error', reject);
});
// 2. Collect results by event_id. One can arrive before the HTTP response does.
const results = new Map<string, Result>();
const waiters = new Map<string, (result: Result) => void>();
const settle = (ok: boolean) => (raw: string) => {
const event: OperationEvent = JSON.parse(raw); // payloads are JSON strings
if (!event.event_id) return; // a status update nobody requested
const waiter = waiters.get(event.event_id);
if (waiter) waiter({ ok, event });
else results.set(event.event_id, { ok, event });
};
socket.on('lot_change', settle(true));
socket.on('gateway_error', settle(false));
// 3. Send the command.
const res = await fetch(`https://api.parklio.com/v2/products/${productId}/state`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ operation: 'open' }),
});
const body = await res.json();
if (!body.success) throw new Error(JSON.stringify(body.errors));
const eventId: string = body.data.event_id;
console.log('Sent, event_id', eventId);
// 4. Wait for the matching event.
const { ok, event } = await new Promise<Result>((resolve) => {
const early = results.get(eventId);
if (early) resolve(early);
else waiters.set(eventId, resolve);
});
console.log(ok ? `Done: ${JSON.stringify(event.data.barrier ?? event.data)}` : `Failed: ${event.message ?? event.type}`);
socket.close();
import json
import os
import threading
import httpx
import socketio
TOKEN = os.environ["PARKLIO_API_TOKEN"]
PRODUCT_ID = os.environ["PRODUCT_ID"]
NS = "/v2/realtime"
sio = socketio.Client()
results = {}
event_id = None
done = threading.Event()
lock = threading.Lock()
def settle(ok):
def handler(raw):
global event_id
event = json.loads(raw) # payloads are JSON strings
if not event.get("event_id"):
return # a status update nobody requested
with lock:
results[event["event_id"]] = (ok, event)
if event["event_id"] == event_id:
done.set()
return handler
sio.on("lot_change", settle(True), namespace=NS)
sio.on("gateway_error", settle(False), namespace=NS)
# 1. Connect first. connect() returns once the server has accepted the token.
sio.connect(
f"https://api.parklio.com?token={TOKEN}",
namespaces=[NS],
socketio_path="socketio",
wait_timeout=10,
)
# 2. Send the command.
res = httpx.post(
f"https://api.parklio.com/v2/products/{PRODUCT_ID}/state",
headers={"Authorization": f"Bearer {TOKEN}"},
json={"operation": "open"},
)
body = res.json()
if not body["success"]:
raise SystemExit(body["errors"])
with lock:
event_id = body["data"]["event_id"]
if event_id in results: # it arrived before the HTTP response
done.set()
print("Sent, event_id", event_id)
# 3. Wait for the matching event.
done.wait(timeout=180)
ok, event = results.get(event_id, (False, {"type": "no result"}))
print("Done" if ok else f"Failed: {event.get('message', event.get('type'))}")
sio.disconnect()
Monitor devices
Alert on offline products, error flags and low batteries from status updates, and watch Gateways. Explained in Monitor devices.
- Node.js
- Python
// The monitor-devices guide: status updates, offline products, Gateways.
import { io } from 'socket.io-client';
const socket = io('https://api.parklio.com/v2/realtime', {
path: '/socketio',
query: { token: process.env.PARKLIO_API_TOKEN },
});
// alert() and resolve() stand for your alerting.
const last = new Map<number, { online: boolean; errorCode: string | null; low: boolean }>();
socket.on('lot_change', (raw: string) => {
const { event_id, data } = JSON.parse(raw);
if (event_id) return; // command results are handled elsewhere
const online = data.status === 'online';
const errorCode: string | null = data.product_error?.error_code ?? null;
const low = isLowBattery(data.battery_status);
const prev = last.get(data.id);
last.set(data.id, { online, errorCode, low });
if (!online && prev?.online) alert(data, 'offline');
if (online && prev?.online === false) resolve(data, 'back online');
if (errorCode && errorCode !== prev?.errorCode) alert(data, `error flags ${errorCode}`, data.product_error);
if (!errorCode && prev?.errorCode) resolve(data, 'error cleared');
if (low && !prev?.low) alert(data, `battery at ${data.battery_status}%`);
});
function isLowBattery(value: unknown) {
// Only barriers report a battery level. 0–100 is percent; above 100 means grid power.
return typeof value === 'number' && value <= 20;
}
socket.on('gateway_status', (raw: string) => {
const { uuid, status } = JSON.parse(raw);
if (status === 'offline') alert({ uuid }, 'Gateway offline');
});
// Your alerting.
function alert(subject: Record<string, any>, what: string, details?: unknown) {
console.warn('ALERT', subject.id ?? subject.uuid, what, details ?? '');
}
function resolve(subject: Record<string, any>, what: string) {
console.info('RESOLVED', subject.id ?? subject.uuid, what);
}
"""The monitor-devices guide: status updates, offline products, Gateways."""
import json
import os
import socketio
NS = "/v2/realtime"
TOKEN = os.environ["PARKLIO_API_TOKEN"]
sio = socketio.Client()
def alert(subject, what, details=None):
print("ALERT", subject.get("id", subject.get("uuid")), what, details or "")
def resolve(subject, what):
print("RESOLVED", subject.get("id", subject.get("uuid")), what)
# alert() and resolve() stand for your alerting.
last = {} # product id -> (online, error_code, low)
def is_low_battery(value):
# Only barriers report a battery level. 0–100 is percent; above 100 means grid power.
return isinstance(value, (int, float)) and value <= 20
@sio.on("lot_change", namespace=NS)
def on_lot_change(raw):
event = json.loads(raw)
if event.get("event_id"):
return # command results are handled elsewhere
data = event["data"]
online = data["status"] == "online"
error_code = (data.get("product_error") or {}).get("error_code")
low = is_low_battery(data.get("battery_status"))
prev_online, prev_code, prev_low = last.get(data["id"], (None, None, False))
last[data["id"]] = (online, error_code, low)
if not online and prev_online:
alert(data, "offline")
if online and prev_online is False:
resolve(data, "back online")
if error_code and error_code != prev_code:
alert(data, f"error flags {error_code}", data["product_error"])
if not error_code and prev_code:
resolve(data, "error cleared")
if low and not prev_low:
alert(data, f"battery at {data['battery_status']}%")
@sio.on("gateway_status", namespace=NS)
def on_gateway_status(raw):
event = json.loads(raw)
if event["status"] == "offline":
alert({"uuid": event["uuid"]}, "Gateway offline")
if __name__ == "__main__":
sio.connect(f"https://api.parklio.com?token={TOKEN}", namespaces=[NS], socketio_path="socketio")
sio.wait()
Create an operation token for your app
Mint a short-lived token for one product, to hand to your app. Explained in Operation tokens for your app.
- Node.js
- Python
// The operation-tokens guide: mint a short-lived token for your app.
export async function createOperationToken(productId: number, minutes: number) {
const start = new Date();
const end = new Date(start.getTime() + minutes * 60_000);
const res = await fetch('https://api.parklio.com/v2/operation-tokens', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PARKLIO_API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_id: productId,
start_time: start.toISOString(),
end_time: end.toISOString(),
}),
});
const body = await res.json();
if (!body.success) throw new Error(`${body.errors[0].code}: ${body.errors[0].message}`);
return body.data;
}
if (import.meta.main) {
const token = await createOperationToken(Number(process.env.PRODUCT_ID), 15);
// Hand these to your app, unchanged.
const { token_open, token_close, token_status, end_time, product } = token;
console.log({ device_id: product.device_id, end_time, token_open, token_close, token_status });
}
import os
from datetime import datetime, timedelta, timezone
import httpx
def create_operation_token(product_id, minutes):
start = datetime.now(timezone.utc)
end = start + timedelta(minutes=minutes)
body = httpx.post(
"https://api.parklio.com/v2/operation-tokens",
headers={"Authorization": f"Bearer {os.environ['PARKLIO_API_TOKEN']}"},
json={
"product_id": product_id,
"start_time": start.isoformat(),
"end_time": end.isoformat(),
},
).json()
if not body["success"]:
error = body["errors"][0]
raise RuntimeError(f"{error['code']}: {error['message']}")
return body["data"]
Whitelist plates and bind them to a product
Add plates to a lot's whitelist, bind them to a product, and check the binding. Explained in Plate-based access.
- Node.js
- Python
// The plate-based-access guide: whitelist plates on a lot, bind them to a product.
const API = 'https://api.parklio.com/v2';
const headers = {
Authorization: `Bearer ${process.env.PARKLIO_API_TOKEN}`,
'Content-Type': 'application/json',
};
async function call(method: string, path: string, body?: unknown) {
const init = body === undefined ? { method, headers } : { method, headers, body: JSON.stringify(body) };
const res = await fetch(`${API}${path}`, init);
const json = await res.json();
if (!json.success) throw new Error(`${path}: ${JSON.stringify(json.errors)}`);
return json;
}
export async function whitelistAndBind(lotId: number, productId: number, plates: string[], until: Date) {
// 1. Whitelist the plates on the lot. One result per plate.
const { data: results } = await call('POST', '/lot-whitelists', {
lot_id: lotId,
name: 'Tenants',
license_plates: plates,
start_time: new Date().toISOString(),
end_time: until.toISOString(),
});
const failed = results.filter((r: { success: boolean }) => !r.success);
if (failed.length) console.warn('Not whitelisted:', failed);
const licenseIds = results.filter((r: { success: boolean }) => r.success).map((r: { license_id: number }) => r.license_id);
// 2. Bind them to the product. Unknown ids are ignored silently...
await call('POST', `/products/${productId}/bind-licenses`, { license_ids: licenseIds });
// 3. ...so check what's bound.
const { data: bound } = await call('GET', `/products/${productId}/bound-licenses?size=300`);
const boundIds = new Set(bound.map((b: { id: number }) => b.id));
return licenseIds.filter((id: number) => !boundIds.has(id)); // ids that didn't bind
}
if (import.meta.main) {
const until = new Date(Date.now() + 30 * 24 * 3600_000);
const missing = await whitelistAndBind(Number(process.env.LOT_ID), Number(process.env.PRODUCT_ID), ['ZG1234AB'], until);
console.log(missing.length ? `Not bound: ${missing}` : 'Whitelisted and bound');
}
"""The plate-based-access guide: whitelist plates on a lot, bind them to a product."""
import os
from datetime import datetime, timedelta, timezone
import httpx
API = "https://api.parklio.com/v2"
HEADERS = {"Authorization": f"Bearer {os.environ['PARKLIO_API_TOKEN']}"}
def call(method, path, body=None):
res = httpx.request(method, f"{API}{path}", headers=HEADERS, json=body)
data = res.json()
if not data["success"]:
raise RuntimeError(f"{path}: {data['errors']}")
return data
def whitelist_and_bind(lot_id, product_id, plates, until):
# 1. Whitelist the plates on the lot. One result per plate.
results = call("POST", "/lot-whitelists", {
"lot_id": lot_id,
"name": "Tenants",
"license_plates": plates,
"start_time": datetime.now(timezone.utc).isoformat(),
"end_time": until.isoformat(),
})["data"]
failed = [r for r in results if not r["success"]]
if failed:
print("Not whitelisted:", failed)
license_ids = [r["license_id"] for r in results if r["success"]]
# 2. Bind them to the product. Unknown ids are ignored silently...
call("POST", f"/products/{product_id}/bind-licenses", {"license_ids": license_ids})
# 3. ...so check what's bound.
bound = call("GET", f"/products/{product_id}/bound-licenses?size=300")["data"]
bound_ids = {b["id"] for b in bound}
return [i for i in license_ids if i not in bound_ids] # ids that didn't bind
if __name__ == "__main__":
until = datetime.now(timezone.utc) + timedelta(days=30)
missing = whitelist_and_bind(
int(os.environ["LOT_ID"]), int(os.environ["PRODUCT_ID"]), ["ZG1234AB"], until
)
print(f"Not bound: {missing}" if missing else "Whitelisted and bound")
Live occupancy
Keep free-space counts per zone up to date from zone_change events, and re-read the
zones every minute for changes that send no event. Explained in
Live occupancy.
- Node.js
- Python
// The live-occupancy guide: read zone counts, keep them live, re-read regularly.
import { io } from 'socket.io-client';
const LOT_ID = Number(process.env.LOT_ID);
const headers = { Authorization: `Bearer ${process.env.PARKLIO_API_TOKEN}` };
const socket = io('https://api.parklio.com/v2/realtime', {
path: '/socketio',
query: { token: process.env.PARKLIO_API_TOKEN },
});
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`);
}
// LPR, payment-machine and recount changes send no event: re-read every minute.
async function readZones(lotId: number) {
const url = `https://api.parklio.com/v2/zones?filter=lot_id:eq:${lotId}&size=300`;
const body = await (await fetch(url, { headers })).json();
for (const zone of body.data) {
zones.set(zone.id, { spot_count: zone.spot_count, occupied_count: zone.occupied_count });
render(zone.id);
}
}
socket.on('connect', () => readZones(LOT_ID).catch(console.error));
setInterval(() => readZones(LOT_ID).catch(console.error), 60_000);
"""The live-occupancy guide: read zone counts, keep them live, re-read regularly."""
import json
import os
import threading
import httpx
import socketio
NS = "/v2/realtime"
TOKEN = os.environ["PARKLIO_API_TOKEN"]
LOT_ID = int(os.environ["LOT_ID"])
zones = {} # zone id -> {"spot_count", "occupied_count"}
lock = threading.Lock()
sio = socketio.Client()
def render(zone_id):
zone = zones[zone_id]
print(f"zone {zone_id}: {zone['spot_count'] - zone['occupied_count']} free")
def remember(zone):
with lock:
zones[zone["id"]] = {
"spot_count": zone["spot_count"],
"occupied_count": zone["occupied_count"],
}
render(zone["id"])
@sio.on("zone_change", namespace=NS)
def on_zone_change(raw):
for zone in json.loads(raw)["zones"]:
remember(zone)
def read_zones(lot_id):
# LPR, payment-machine and recount changes send no event: re-read regularly.
res = httpx.get(
f"https://api.parklio.com/v2/zones?filter=lot_id:eq:{lot_id}&size=300",
headers={"Authorization": f"Bearer {TOKEN}"},
)
for zone in res.json()["data"]:
remember(zone)
@sio.event(namespace=NS)
def connect():
read_zones(LOT_ID) # on the first connect and after every reconnect
if __name__ == "__main__":
sio.connect(f"https://api.parklio.com?token={TOKEN}", namespaces=[NS], socketio_path="socketio")
while True:
sio.sleep(60)
read_zones(LOT_ID)
Next
- Patterns: the
PendingOperationshelper, deduplication and resync. - Troubleshooting